From 5500cbf0e420a6d643835ec05f35abb170e3e443 Mon Sep 17 00:00:00 2001 From: Jes Cok Date: Thu, 25 Sep 2025 04:41:12 +0000 Subject: [PATCH 01/63] debug/elf: prevent offset overflow When applying relocations, a malformed ELF file can provide an offset that, when added to the relocation size, overflows. This wrapped-around value could then incorrectly pass the bounds check, leading to a panic when the slice is accessed with the original large offset. This change eliminates the manual bounds and overflow checks and writes a relocation to slice by calling putUint. The putUint helper function centralizes the logic for validating slice access, correctly handling both out-of-bounds and integer overflow conditions. This simplifies the relocation code and improves robustness when parsing malformed ELF files. Fixes #75516 Change-Id: I00d806bf5501a9bf70200585ba4fd0475d7b2ddc GitHub-Last-Rev: 49144311d31fecc63cb81b6c31bf9a206acb0596 GitHub-Pull-Request: golang/go#75522 Reviewed-on: https://go-review.googlesource.com/c/go/+/705075 Reviewed-by: Florian Lehner LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao Auto-Submit: Ian Lance Taylor Reviewed-by: Michael Knyszek Reviewed-by: Ian Lance Taylor Commit-Queue: Ian Lance Taylor --- src/debug/elf/file.go | 160 +++++++++++++++--------------------------- 1 file changed, 57 insertions(+), 103 deletions(-) diff --git a/src/debug/elf/file.go b/src/debug/elf/file.go index 50452b5bef45f4..1d56a06c3fb221 100644 --- a/src/debug/elf/file.go +++ b/src/debug/elf/file.go @@ -25,6 +25,7 @@ import ( "internal/saferio" "internal/zstd" "io" + "math" "os" "strings" "unsafe" @@ -830,17 +831,9 @@ func (f *File) applyRelocationsAMD64(dst []byte, rels []byte) error { switch t { case R_X86_64_64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_X86_64_32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -872,12 +865,7 @@ func (f *File) applyRelocations386(dst []byte, rels []byte) error { sym := &symbols[symNo-1] if t == R_386_32 { - if rel.Off+4 >= uint32(len(dst)) { - continue - } - val := f.ByteOrder.Uint32(dst[rel.Off : rel.Off+4]) - val += uint32(sym.Value) - f.ByteOrder.PutUint32(dst[rel.Off:rel.Off+4], val) + putUint(f.ByteOrder, dst, uint64(rel.Off), 4, sym.Value, 0, true) } } @@ -910,12 +898,7 @@ func (f *File) applyRelocationsARM(dst []byte, rels []byte) error { switch t { case R_ARM_ABS32: - if rel.Off+4 >= uint32(len(dst)) { - continue - } - val := f.ByteOrder.Uint32(dst[rel.Off : rel.Off+4]) - val += uint32(sym.Value) - f.ByteOrder.PutUint32(dst[rel.Off:rel.Off+4], val) + putUint(f.ByteOrder, dst, uint64(rel.Off), 4, sym.Value, 0, true) } } @@ -955,17 +938,9 @@ func (f *File) applyRelocationsARM64(dst []byte, rels []byte) error { switch t { case R_AARCH64_ABS64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_AARCH64_ABS32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1001,11 +976,7 @@ func (f *File) applyRelocationsPPC(dst []byte, rels []byte) error { switch t { case R_PPC_ADDR32: - if rela.Off+4 >= uint32(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, uint64(rela.Off), 4, sym.Value, 0, false) } } @@ -1041,17 +1012,9 @@ func (f *File) applyRelocationsPPC64(dst []byte, rels []byte) error { switch t { case R_PPC64_ADDR64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_PPC64_ADDR32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1084,12 +1047,7 @@ func (f *File) applyRelocationsMIPS(dst []byte, rels []byte) error { switch t { case R_MIPS_32: - if rel.Off+4 >= uint32(len(dst)) { - continue - } - val := f.ByteOrder.Uint32(dst[rel.Off : rel.Off+4]) - val += uint32(sym.Value) - f.ByteOrder.PutUint32(dst[rel.Off:rel.Off+4], val) + putUint(f.ByteOrder, dst, uint64(rel.Off), 4, sym.Value, 0, true) } } @@ -1132,17 +1090,9 @@ func (f *File) applyRelocationsMIPS64(dst []byte, rels []byte) error { switch t { case R_MIPS_64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_MIPS_32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1180,17 +1130,9 @@ func (f *File) applyRelocationsLOONG64(dst []byte, rels []byte) error { switch t { case R_LARCH_64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_LARCH_32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1226,17 +1168,9 @@ func (f *File) applyRelocationsRISCV64(dst []byte, rels []byte) error { switch t { case R_RISCV_64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_RISCV_32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1272,17 +1206,9 @@ func (f *File) applyRelocationss390x(dst []byte, rels []byte) error { switch t { case R_390_64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) case R_390_32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1318,17 +1244,10 @@ func (f *File) applyRelocationsSPARC64(dst []byte, rels []byte) error { switch t { case R_SPARC_64, R_SPARC_UA64: - if rela.Off+8 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val64 := sym.Value + uint64(rela.Addend) - f.ByteOrder.PutUint64(dst[rela.Off:rela.Off+8], val64) + putUint(f.ByteOrder, dst, rela.Off, 8, sym.Value, rela.Addend, false) + case R_SPARC_32, R_SPARC_UA32: - if rela.Off+4 >= uint64(len(dst)) || rela.Addend < 0 { - continue - } - val32 := uint32(sym.Value) + uint32(rela.Addend) - f.ByteOrder.PutUint32(dst[rela.Off:rela.Off+4], val32) + putUint(f.ByteOrder, dst, rela.Off, 4, sym.Value, rela.Addend, false) } } @@ -1903,3 +1822,38 @@ type nobitsSectionReader struct{} func (*nobitsSectionReader) ReadAt(p []byte, off int64) (n int, err error) { return 0, errors.New("unexpected read from SHT_NOBITS section") } + +// putUint writes a relocation to slice +// at offset start of length length (4 or 8 bytes), +// adding sym+addend to the existing value if readUint is true, +// or just writing sym+addend if readUint is false. +// If the write would extend beyond the end of slice, putUint does nothing. +// If the addend is negative, putUint does nothing. +// If the addition would overflow, putUint does nothing. +func putUint(byteOrder binary.ByteOrder, slice []byte, start, length, sym uint64, addend int64, readUint bool) { + if start+length > uint64(len(slice)) || math.MaxUint64-start < length { + return + } + if addend < 0 { + return + } + + s := slice[start : start+length] + + switch length { + case 4: + ae := uint32(addend) + if readUint { + ae += byteOrder.Uint32(s) + } + byteOrder.PutUint32(s, uint32(sym)+ae) + case 8: + ae := uint64(addend) + if readUint { + ae += byteOrder.Uint64(s) + } + byteOrder.PutUint64(s, sym+ae) + default: + panic("can't happen") + } +} From 6d51f932575284d6e78baa5e98f47f737a9f5b19 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 29 Sep 2025 14:14:28 +0200 Subject: [PATCH 02/63] runtime: jump instead of branch in netbsd/arm64 entry point CL 706175 removed the NOFRAME directive from _rt0_arm64_netbsd but did not change the BL instruction to a JMP instruction. This causes the frame pointer to be stored on the stack, this making direct load from RSP to be off by 8 bytes. Cq-Include-Trybots: luci.golang.try:gotip-netbsd-arm64 Change-Id: I0c212fbaba74cfce508f961090dc6e66154c3054 Reviewed-on: https://go-review.googlesource.com/c/go/+/707675 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Pratt Reviewed-by: Cherry Mui --- src/runtime/rt0_netbsd_arm64.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/rt0_netbsd_arm64.s b/src/runtime/rt0_netbsd_arm64.s index 07fb0a1240aec5..80802209b78f12 100644 --- a/src/runtime/rt0_netbsd_arm64.s +++ b/src/runtime/rt0_netbsd_arm64.s @@ -7,7 +7,7 @@ TEXT _rt0_arm64_netbsd(SB),NOSPLIT,$0 MOVD 0(RSP), R0 // argc ADD $8, RSP, R1 // argv - BL main(SB) + JMP main(SB) // When building with -buildmode=c-shared, this symbol is called when the shared // library is loaded. From d42d56b764f4c8b06aaa2de2dc9c1d2171e79490 Mon Sep 17 00:00:00 2001 From: apocelipes Date: Wed, 24 Sep 2025 03:23:03 +0000 Subject: [PATCH 03/63] encoding/xml: make use of reflect.TypeAssert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To make the code more readable and improve performance: goos: darwin goarch: arm64 pkg: encoding/xml cpu: Apple M4 │ old │ new │ │ sec/op │ sec/op vs base │ Marshal-10 1.902µ ± 1% 1.496µ ± 1% -21.37% (p=0.000 n=10) Unmarshal-10 3.877µ ± 1% 3.418µ ± 2% -11.84% (p=0.000 n=10) HTMLAutoClose-10 1.314µ ± 3% 1.333µ ± 1% ~ (p=0.270 n=10) geomean 2.132µ 1.896µ -11.07% │ old │ new │ │ B/op │ B/op vs base │ Marshal-10 5.570Ki ± 0% 5.570Ki ± 0% ~ (p=1.000 n=10) ¹ Unmarshal-10 7.586Ki ± 0% 7.555Ki ± 0% -0.41% (p=0.000 n=10) HTMLAutoClose-10 3.496Ki ± 0% 3.496Ki ± 0% ~ (p=1.000 n=10) ¹ geomean 5.286Ki 5.279Ki -0.14% ¹ all samples are equal │ old │ new │ │ allocs/op │ allocs/op vs base │ Marshal-10 23.00 ± 0% 23.00 ± 0% ~ (p=1.000 n=10) ¹ Unmarshal-10 185.0 ± 0% 184.0 ± 0% -0.54% (p=0.000 n=10) HTMLAutoClose-10 93.00 ± 0% 93.00 ± 0% ~ (p=1.000 n=10) ¹ geomean 73.42 73.28 -0.18% ¹ all samples are equal Updates #62121 Change-Id: Ie458e7458d4358c380374571d380ca3b65ca87bb GitHub-Last-Rev: bb6bb3039328ca1d53ee3d56fd6597109ed76b09 GitHub-Pull-Request: golang/go#75483 Reviewed-on: https://go-review.googlesource.com/c/go/+/704215 LUCI-TryBot-Result: Go LUCI Auto-Submit: Keith Randall Reviewed-by: Keith Randall Reviewed-by: Keith Randall Reviewed-by: Carlos Amedee --- src/encoding/xml/marshal.go | 127 ++++++++++++++++++++---------------- src/encoding/xml/read.go | 77 +++++++++++++--------- 2 files changed, 117 insertions(+), 87 deletions(-) diff --git a/src/encoding/xml/marshal.go b/src/encoding/xml/marshal.go index 133503fa2de41c..13fbeeeedc75ce 100644 --- a/src/encoding/xml/marshal.go +++ b/src/encoding/xml/marshal.go @@ -416,12 +416,6 @@ func (p *printer) popPrefix() { } } -var ( - marshalerType = reflect.TypeFor[Marshaler]() - marshalerAttrType = reflect.TypeFor[MarshalerAttr]() - textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]() -) - // marshalValue writes one or more XML elements representing val. // If val was obtained from a struct field, finfo must have its details. func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplate *StartElement) error { @@ -450,24 +444,32 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat typ := val.Type() // Check for marshaler. - if val.CanInterface() && typ.Implements(marshalerType) { - return p.marshalInterface(val.Interface().(Marshaler), defaultStart(typ, finfo, startTemplate)) + if val.CanInterface() { + if marshaler, ok := reflect.TypeAssert[Marshaler](val); ok { + return p.marshalInterface(marshaler, defaultStart(typ, finfo, startTemplate)) + } } if val.CanAddr() { pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(marshalerType) { - return p.marshalInterface(pv.Interface().(Marshaler), defaultStart(pv.Type(), finfo, startTemplate)) + if pv.CanInterface() { + if marshaler, ok := reflect.TypeAssert[Marshaler](pv); ok { + return p.marshalInterface(marshaler, defaultStart(pv.Type(), finfo, startTemplate)) + } } } // Check for text marshaler. - if val.CanInterface() && typ.Implements(textMarshalerType) { - return p.marshalTextInterface(val.Interface().(encoding.TextMarshaler), defaultStart(typ, finfo, startTemplate)) + if val.CanInterface() { + if textMarshaler, ok := reflect.TypeAssert[encoding.TextMarshaler](val); ok { + return p.marshalTextInterface(textMarshaler, defaultStart(typ, finfo, startTemplate)) + } } if val.CanAddr() { pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(textMarshalerType) { - return p.marshalTextInterface(pv.Interface().(encoding.TextMarshaler), defaultStart(pv.Type(), finfo, startTemplate)) + if pv.CanInterface() { + if textMarshaler, ok := reflect.TypeAssert[encoding.TextMarshaler](pv); ok { + return p.marshalTextInterface(textMarshaler, defaultStart(pv.Type(), finfo, startTemplate)) + } } } @@ -503,7 +505,7 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat start.Name.Space, start.Name.Local = xmlname.xmlns, xmlname.name } else { fv := xmlname.value(val, dontInitNilPointers) - if v, ok := fv.Interface().(Name); ok && v.Local != "" { + if v, ok := reflect.TypeAssert[Name](fv); ok && v.Local != "" { start.Name = v } } @@ -580,21 +582,9 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat // marshalAttr marshals an attribute with the given name and value, adding to start.Attr. func (p *printer) marshalAttr(start *StartElement, name Name, val reflect.Value) error { - if val.CanInterface() && val.Type().Implements(marshalerAttrType) { - attr, err := val.Interface().(MarshalerAttr).MarshalXMLAttr(name) - if err != nil { - return err - } - if attr.Name.Local != "" { - start.Attr = append(start.Attr, attr) - } - return nil - } - - if val.CanAddr() { - pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(marshalerAttrType) { - attr, err := pv.Interface().(MarshalerAttr).MarshalXMLAttr(name) + if val.CanInterface() { + if marshaler, ok := reflect.TypeAssert[MarshalerAttr](val); ok { + attr, err := marshaler.MarshalXMLAttr(name) if err != nil { return err } @@ -605,19 +595,25 @@ func (p *printer) marshalAttr(start *StartElement, name Name, val reflect.Value) } } - if val.CanInterface() && val.Type().Implements(textMarshalerType) { - text, err := val.Interface().(encoding.TextMarshaler).MarshalText() - if err != nil { - return err + if val.CanAddr() { + pv := val.Addr() + if pv.CanInterface() { + if marshaler, ok := reflect.TypeAssert[MarshalerAttr](pv); ok { + attr, err := marshaler.MarshalXMLAttr(name) + if err != nil { + return err + } + if attr.Name.Local != "" { + start.Attr = append(start.Attr, attr) + } + return nil + } } - start.Attr = append(start.Attr, Attr{name, string(text)}) - return nil } - if val.CanAddr() { - pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(textMarshalerType) { - text, err := pv.Interface().(encoding.TextMarshaler).MarshalText() + if val.CanInterface() { + if textMarshaler, ok := reflect.TypeAssert[encoding.TextMarshaler](val); ok { + text, err := textMarshaler.MarshalText() if err != nil { return err } @@ -626,6 +622,20 @@ func (p *printer) marshalAttr(start *StartElement, name Name, val reflect.Value) } } + if val.CanAddr() { + pv := val.Addr() + if pv.CanInterface() { + if textMarshaler, ok := reflect.TypeAssert[encoding.TextMarshaler](pv); ok { + text, err := textMarshaler.MarshalText() + if err != nil { + return err + } + start.Attr = append(start.Attr, Attr{name, string(text)}) + return nil + } + } + } + // Dereference or skip nil pointer, interface values. switch val.Kind() { case reflect.Pointer, reflect.Interface: @@ -647,7 +657,8 @@ func (p *printer) marshalAttr(start *StartElement, name Name, val reflect.Value) } if val.Type() == attrType { - start.Attr = append(start.Attr, val.Interface().(Attr)) + attr, _ := reflect.TypeAssert[Attr](val) + start.Attr = append(start.Attr, attr) return nil } @@ -854,20 +865,9 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { if err := s.trim(finfo.parents); err != nil { return err } - if vf.CanInterface() && vf.Type().Implements(textMarshalerType) { - data, err := vf.Interface().(encoding.TextMarshaler).MarshalText() - if err != nil { - return err - } - if err := emit(p, data); err != nil { - return err - } - continue - } - if vf.CanAddr() { - pv := vf.Addr() - if pv.CanInterface() && pv.Type().Implements(textMarshalerType) { - data, err := pv.Interface().(encoding.TextMarshaler).MarshalText() + if vf.CanInterface() { + if textMarshaler, ok := reflect.TypeAssert[encoding.TextMarshaler](vf); ok { + data, err := textMarshaler.MarshalText() if err != nil { return err } @@ -877,6 +877,21 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { continue } } + if vf.CanAddr() { + pv := vf.Addr() + if pv.CanInterface() { + if textMarshaler, ok := reflect.TypeAssert[encoding.TextMarshaler](pv); ok { + data, err := textMarshaler.MarshalText() + if err != nil { + return err + } + if err := emit(p, data); err != nil { + return err + } + continue + } + } + } var scratch [64]byte vf = indirect(vf) @@ -902,7 +917,7 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { return err } case reflect.Slice: - if elem, ok := vf.Interface().([]byte); ok { + if elem, ok := reflect.TypeAssert[[]byte](vf); ok { if err := emit(p, elem); err != nil { return err } diff --git a/src/encoding/xml/read.go b/src/encoding/xml/read.go index af25c20f0618dc..d3cb74b2c4311a 100644 --- a/src/encoding/xml/read.go +++ b/src/encoding/xml/read.go @@ -255,28 +255,36 @@ func (d *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error { } val = val.Elem() } - if val.CanInterface() && val.Type().Implements(unmarshalerAttrType) { + if val.CanInterface() { // This is an unmarshaler with a non-pointer receiver, // so it's likely to be incorrect, but we do what we're told. - return val.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr) + if unmarshaler, ok := reflect.TypeAssert[UnmarshalerAttr](val); ok { + return unmarshaler.UnmarshalXMLAttr(attr) + } } if val.CanAddr() { pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(unmarshalerAttrType) { - return pv.Interface().(UnmarshalerAttr).UnmarshalXMLAttr(attr) + if pv.CanInterface() { + if unmarshaler, ok := reflect.TypeAssert[UnmarshalerAttr](pv); ok { + return unmarshaler.UnmarshalXMLAttr(attr) + } } } // Not an UnmarshalerAttr; try encoding.TextUnmarshaler. - if val.CanInterface() && val.Type().Implements(textUnmarshalerType) { + if val.CanInterface() { // This is an unmarshaler with a non-pointer receiver, // so it's likely to be incorrect, but we do what we're told. - return val.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value)) + if textUnmarshaler, ok := reflect.TypeAssert[encoding.TextUnmarshaler](val); ok { + return textUnmarshaler.UnmarshalText([]byte(attr.Value)) + } } if val.CanAddr() { pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) { - return pv.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(attr.Value)) + if pv.CanInterface() { + if textUnmarshaler, ok := reflect.TypeAssert[encoding.TextUnmarshaler](pv); ok { + return textUnmarshaler.UnmarshalText([]byte(attr.Value)) + } } } @@ -303,12 +311,7 @@ func (d *Decoder) unmarshalAttr(val reflect.Value, attr Attr) error { return copyValue(val, []byte(attr.Value)) } -var ( - attrType = reflect.TypeFor[Attr]() - unmarshalerType = reflect.TypeFor[Unmarshaler]() - unmarshalerAttrType = reflect.TypeFor[UnmarshalerAttr]() - textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]() -) +var attrType = reflect.TypeFor[Attr]() const ( maxUnmarshalDepth = 10000 @@ -352,27 +355,35 @@ func (d *Decoder) unmarshal(val reflect.Value, start *StartElement, depth int) e val = val.Elem() } - if val.CanInterface() && val.Type().Implements(unmarshalerType) { + if val.CanInterface() { // This is an unmarshaler with a non-pointer receiver, // so it's likely to be incorrect, but we do what we're told. - return d.unmarshalInterface(val.Interface().(Unmarshaler), start) + if unmarshaler, ok := reflect.TypeAssert[Unmarshaler](val); ok { + return d.unmarshalInterface(unmarshaler, start) + } } if val.CanAddr() { pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(unmarshalerType) { - return d.unmarshalInterface(pv.Interface().(Unmarshaler), start) + if pv.CanInterface() { + if unmarshaler, ok := reflect.TypeAssert[Unmarshaler](pv); ok { + return d.unmarshalInterface(unmarshaler, start) + } } } - if val.CanInterface() && val.Type().Implements(textUnmarshalerType) { - return d.unmarshalTextInterface(val.Interface().(encoding.TextUnmarshaler)) + if val.CanInterface() { + if textUnmarshaler, ok := reflect.TypeAssert[encoding.TextUnmarshaler](val); ok { + return d.unmarshalTextInterface(textUnmarshaler) + } } if val.CanAddr() { pv := val.Addr() - if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) { - return d.unmarshalTextInterface(pv.Interface().(encoding.TextUnmarshaler)) + if pv.CanInterface() { + if textUnmarshaler, ok := reflect.TypeAssert[encoding.TextUnmarshaler](pv); ok { + return d.unmarshalTextInterface(textUnmarshaler) + } } } @@ -453,7 +464,7 @@ func (d *Decoder) unmarshal(val reflect.Value, start *StartElement, depth int) e return UnmarshalError(e) } fv := finfo.value(sv, initNilPointers) - if _, ok := fv.Interface().(Name); ok { + if _, ok := reflect.TypeAssert[Name](fv); ok { fv.Set(reflect.ValueOf(start.Name)) } } @@ -578,20 +589,24 @@ Loop: } } - if saveData.IsValid() && saveData.CanInterface() && saveData.Type().Implements(textUnmarshalerType) { - if err := saveData.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil { - return err + if saveData.IsValid() && saveData.CanInterface() { + if textUnmarshaler, ok := reflect.TypeAssert[encoding.TextUnmarshaler](saveData); ok { + if err := textUnmarshaler.UnmarshalText(data); err != nil { + return err + } + saveData = reflect.Value{} } - saveData = reflect.Value{} } if saveData.IsValid() && saveData.CanAddr() { pv := saveData.Addr() - if pv.CanInterface() && pv.Type().Implements(textUnmarshalerType) { - if err := pv.Interface().(encoding.TextUnmarshaler).UnmarshalText(data); err != nil { - return err + if pv.CanInterface() { + if textUnmarshaler, ok := reflect.TypeAssert[encoding.TextUnmarshaler](pv); ok { + if err := textUnmarshaler.UnmarshalText(data); err != nil { + return err + } + saveData = reflect.Value{} } - saveData = reflect.Value{} } } From fe3ba74b9e6e3385cbf7c2f3a9c0b72baeac4b01 Mon Sep 17 00:00:00 2001 From: Richard Miller Date: Sat, 27 Sep 2025 19:45:36 +0100 Subject: [PATCH 04/63] cmd/link: skip TestFlagW on platforms without DWARF symbol table As with other DWARF tests, don't run TestFlagW on platforms where executables don't have a DWARF symbol table. Fixes #75585 Change-Id: I81014bf59b15e30ac1b2a7d24a52f9647db46c26 Reviewed-on: https://go-review.googlesource.com/c/go/+/706418 Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- src/cmd/link/dwarf_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cmd/link/dwarf_test.go b/src/cmd/link/dwarf_test.go index 6a60a746a5b48b..4fce358e602de5 100644 --- a/src/cmd/link/dwarf_test.go +++ b/src/cmd/link/dwarf_test.go @@ -364,6 +364,10 @@ func TestFlagW(t *testing.T) { if runtime.GOOS == "aix" { t.Skip("internal/xcoff cannot parse file without symbol table") } + if !platform.ExecutableHasDWARF(runtime.GOOS, runtime.GOARCH) { + t.Skipf("skipping on %s/%s: no DWARF symbol table in executables", runtime.GOOS, runtime.GOARCH) + } + t.Parallel() tmpdir := t.TempDir() From ae8eba071b228dd9e05de0b0c338f3d941a0a43f Mon Sep 17 00:00:00 2001 From: Ian Lance Taylor Date: Sun, 28 Sep 2025 21:25:24 -0700 Subject: [PATCH 05/63] cmd/link: use correct length for pcln.cutab The pcln.cutab slice holds uint32 elements, as can be seen in the runtime.moduledata type. The slice was being created with the len (and cap) set to the size of the slice, which means that the count was four times too large. This patch sets the correct len/cap. This doesn't matter for the runtime because nothing looks at the len of cutab. Since the incorrect len is larger, all valid indexes remain valid. Using the correct length means that more invalid indexes will be caught at run time, but such cases are unlikely. Still, using the correct len is less confusing. While we're here use the simpler sliceSym for pcln.pclntab. Change-Id: I09f680b3287467120d994b171c86c784085e3d27 Reviewed-on: https://go-review.googlesource.com/c/go/+/707595 LUCI-TryBot-Result: Go LUCI Reviewed-by: Cherry Mui Reviewed-by: Carlos Amedee Auto-Submit: Ian Lance Taylor --- src/cmd/link/internal/ld/symtab.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd/link/internal/ld/symtab.go b/src/cmd/link/internal/ld/symtab.go index 759262286d39d8..2c999ccc4e3a19 100644 --- a/src/cmd/link/internal/ld/symtab.go +++ b/src/cmd/link/internal/ld/symtab.go @@ -645,7 +645,7 @@ func (ctxt *Link) symtab(pcln *pclntab) []sym.SymKind { sliceSym(pcln.funcnametab) // The cutab slice - sliceSym(pcln.cutab) + slice(pcln.cutab, uint64(ldr.SymSize(pcln.cutab))/4) // The filetab slice sliceSym(pcln.filetab) @@ -654,7 +654,7 @@ func (ctxt *Link) symtab(pcln *pclntab) []sym.SymKind { sliceSym(pcln.pctab) // The pclntab slice - slice(pcln.pclntab, uint64(ldr.SymSize(pcln.pclntab))) + sliceSym(pcln.pclntab) // The ftab slice slice(pcln.pclntab, uint64(pcln.nfunc+1)) From 047c2ab841e2d2233d0bef420d1b5ecb545a380a Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Fri, 26 Sep 2025 09:45:08 -0400 Subject: [PATCH 06/63] cmd/link: don't pass -Wl,-S on Solaris Solaris linker's -S has a different meaning. Fixes #75637. Change-Id: I51e641d5bc6d7f64ab5aa280090c70ec787a1fbf Reviewed-on: https://go-review.googlesource.com/c/go/+/707096 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase --- src/cmd/link/dwarf_test.go | 2 +- src/cmd/link/internal/ld/lib.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd/link/dwarf_test.go b/src/cmd/link/dwarf_test.go index 4fce358e602de5..56a076002a92d4 100644 --- a/src/cmd/link/dwarf_test.go +++ b/src/cmd/link/dwarf_test.go @@ -386,7 +386,7 @@ func TestFlagW(t *testing.T) { {"-s", false}, // -s implies -w {"-s -w=0", true}, // -w=0 negates the implied -w } - if testenv.HasCGO() { + if testenv.HasCGO() && runtime.GOOS != "solaris" { // Solaris linker doesn't support the -S flag tests = append(tests, testCase{"-w -linkmode=external", false}, testCase{"-s -linkmode=external", false}, diff --git a/src/cmd/link/internal/ld/lib.go b/src/cmd/link/internal/ld/lib.go index 8d2763bb57f31a..c7596d535e0ee5 100644 --- a/src/cmd/link/internal/ld/lib.go +++ b/src/cmd/link/internal/ld/lib.go @@ -1452,7 +1452,7 @@ func (ctxt *Link) hostlink() { argv = append(argv, "-s") } } else if *FlagW { - if !ctxt.IsAIX() { // The AIX linker's -S has different meaning + if !ctxt.IsAIX() && !ctxt.IsSolaris() { // The AIX and Solaris linkers' -S has different meaning argv = append(argv, "-Wl,-S") // suppress debugging symbols } } From 4e9006a716533fe1c7ee08df02dfc73078f7dc19 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Mon, 29 Sep 2025 10:11:56 -0700 Subject: [PATCH 07/63] crypto/tls: quote protocols in ALPN error message Quote the protocols sent by the client when returning the ALPN negotiation error message. Fixes CVE-2025-58189 Fixes #75652 Change-Id: Ie7b3a1ed0b6efcc1705b71f0f1e8417126661330 Reviewed-on: https://go-review.googlesource.com/c/go/+/707776 Auto-Submit: Roland Shoemaker Reviewed-by: Neal Patel Reviewed-by: Nicholas Husin Auto-Submit: Nicholas Husin Reviewed-by: Nicholas Husin TryBot-Bypass: Roland Shoemaker Reviewed-by: Daniel McCarney --- src/crypto/tls/handshake_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/tls/handshake_server.go b/src/crypto/tls/handshake_server.go index 1e0b5f06672d15..088c66fadb2a44 100644 --- a/src/crypto/tls/handshake_server.go +++ b/src/crypto/tls/handshake_server.go @@ -357,7 +357,7 @@ func negotiateALPN(serverProtos, clientProtos []string, quic bool) (string, erro if http11fallback { return "", nil } - return "", fmt.Errorf("tls: client requested unsupported application protocols (%s)", clientProtos) + return "", fmt.Errorf("tls: client requested unsupported application protocols (%q)", clientProtos) } // supportsECDHE returns whether ECDHE key exchanges can be used with this From 4b7773356515c178f0af859b952b4b3a78f0813d Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 29 Sep 2025 08:37:35 +0200 Subject: [PATCH 08/63] internal/syscall/windows: regenerate GetFileSizeEx GetFileSizeEx was generated before mkwinsyscall was updated to use SyscallN. Regenerate to use the new style. Fixes #75642 Change-Id: Ia473a167633b67fb75b5762d693848ecee425a7e Reviewed-on: https://go-review.googlesource.com/c/go/+/707615 Reviewed-by: Roland Shoemaker Auto-Submit: Dmitri Shuralyov Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov --- src/internal/syscall/windows/zsyscall_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go index fb8bc80629a626..70c4d76dff0d0f 100644 --- a/src/internal/syscall/windows/zsyscall_windows.go +++ b/src/internal/syscall/windows/zsyscall_windows.go @@ -328,7 +328,7 @@ func GetFileInformationByHandleEx(handle syscall.Handle, class uint32, info *byt } func GetFileSizeEx(handle syscall.Handle, size *int64) (err error) { - r1, _, e1 := syscall.Syscall(procGetFileSizeEx.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(size)), 0) + r1, _, e1 := syscall.SyscallN(procGetFileSizeEx.Addr(), uintptr(handle), uintptr(unsafe.Pointer(size))) if r1 == 0 { err = errnoErr(e1) } From eaf2345256613dfbda7e8e69e5f845c4209246c6 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Mon, 22 Sep 2025 15:48:36 +0200 Subject: [PATCH 09/63] cmd/link: use a .def file to mark exported symbols on Windows Binutils defaults to exporting all symbols when building a Windows DLL. To avoid that we were marking symbols with __declspec(dllexport) in the cgo-generated headers, which instructs ld to export only those symbols. However, that approach makes the headers hard to reuse when importing the resulting DLL into other projects, as imported symbols should be marked with __declspec(dllimport). A better approach is to generate a .def file listing the symbols to export, which gets the same effect without having to modify the headers. Updates #30674 Fixes #56994 Change-Id: I22bd0aa079e2be4ae43b13d893f6b804eaeddabf Reviewed-on: https://go-review.googlesource.com/c/go/+/705776 Reviewed-by: Michael Knyszek Reviewed-by: Junyang Shao Reviewed-by: Than McIntosh Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI --- .../cgo/internal/testcshared/cshared_test.go | 75 ++++++++----------- src/cmd/cgo/out.go | 6 +- src/cmd/link/internal/ld/lib.go | 9 ++- src/cmd/link/internal/ld/pe.go | 44 +++++++++++ src/cmd/link/internal/ld/xcoff.go | 5 +- src/cmd/link/internal/loader/loader.go | 13 ++++ src/runtime/cgo/gcc_libinit_windows.c | 9 --- src/runtime/cgo/windows.go | 22 ++++++ 8 files changed, 118 insertions(+), 65 deletions(-) create mode 100644 src/runtime/cgo/windows.go diff --git a/src/cmd/cgo/internal/testcshared/cshared_test.go b/src/cmd/cgo/internal/testcshared/cshared_test.go index f1c30f8f9a2b2a..2ce705adba44f3 100644 --- a/src/cmd/cgo/internal/testcshared/cshared_test.go +++ b/src/cmd/cgo/internal/testcshared/cshared_test.go @@ -8,6 +8,7 @@ import ( "bufio" "bytes" "cmd/cgo/internal/cgotest" + "cmp" "debug/elf" "debug/pe" "encoding/binary" @@ -272,7 +273,7 @@ func createHeaders() error { // which results in the linkers output implib getting overwritten at each step. So instead build the // import library the traditional way, using a def file. err = os.WriteFile("libgo.def", - []byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n\t_cgo_dummy_export\n"), + []byte("LIBRARY libgo.dll\nEXPORTS\n\tDidInitRun\n\tDidMainRun\n\tDivu\n\tFromPkg\n"), 0644) if err != nil { return fmt.Errorf("unable to write def file: %v", err) @@ -375,9 +376,23 @@ func TestExportedSymbols(t *testing.T) { } } -func checkNumberOfExportedFunctionsWindows(t *testing.T, prog string, exportedFunctions int, wantAll bool) { +func checkNumberOfExportedSymbolsWindows(t *testing.T, exportedSymbols int, wantAll bool) { + t.Parallel() tmpdir := t.TempDir() + prog := ` +package main +import "C" +func main() {} +` + + for i := range exportedSymbols { + prog += fmt.Sprintf(` +//export GoFunc%d +func GoFunc%d() {} +`, i, i) + } + srcfile := filepath.Join(tmpdir, "test.go") objfile := filepath.Join(tmpdir, "test.dll") if err := os.WriteFile(srcfile, []byte(prog), 0666); err != nil { @@ -443,18 +458,19 @@ func checkNumberOfExportedFunctionsWindows(t *testing.T, prog string, exportedFu t.Fatalf("binary.Read failed: %v", err) } - // Only the two exported functions and _cgo_dummy_export should be exported. + exportedSymbols = cmp.Or(exportedSymbols, 1) // _cgo_stub_export is exported if there are no other symbols exported + // NumberOfNames is the number of functions exported with a unique name. // NumberOfFunctions can be higher than that because it also counts // functions exported only by ordinal, a unique number asigned by the linker, // and linkers might add an unknown number of their own ordinal-only functions. if wantAll { - if e.NumberOfNames <= uint32(exportedFunctions) { - t.Errorf("got %d exported names, want > %d", e.NumberOfNames, exportedFunctions) + if e.NumberOfNames <= uint32(exportedSymbols) { + t.Errorf("got %d exported names, want > %d", e.NumberOfNames, exportedSymbols) } } else { - if e.NumberOfNames > uint32(exportedFunctions) { - t.Errorf("got %d exported names, want <= %d", e.NumberOfNames, exportedFunctions) + if e.NumberOfNames != uint32(exportedSymbols) { + t.Errorf("got %d exported names, want %d", e.NumberOfNames, exportedSymbols) } } } @@ -470,43 +486,14 @@ func TestNumberOfExportedFunctions(t *testing.T) { t.Parallel() - const prog0 = ` -package main - -import "C" - -func main() { -} -` - - const prog2 = ` -package main - -import "C" - -//export GoFunc -func GoFunc() { - println(42) -} - -//export GoFunc2 -func GoFunc2() { - println(24) -} - -func main() { -} -` - // All programs export _cgo_dummy_export, so add 1 to the expected counts. - t.Run("OnlyExported/0", func(t *testing.T) { - checkNumberOfExportedFunctionsWindows(t, prog0, 0+1, false) - }) - t.Run("OnlyExported/2", func(t *testing.T) { - checkNumberOfExportedFunctionsWindows(t, prog2, 2+1, false) - }) - t.Run("All", func(t *testing.T) { - checkNumberOfExportedFunctionsWindows(t, prog2, 2+1, true) - }) + for i := range 3 { + t.Run(fmt.Sprintf("OnlyExported/%d", i), func(t *testing.T) { + checkNumberOfExportedSymbolsWindows(t, i, false) + }) + t.Run(fmt.Sprintf("All/%d", i), func(t *testing.T) { + checkNumberOfExportedSymbolsWindows(t, i, true) + }) + } } // test1: shared library can be dynamically loaded and exported symbols are accessible. diff --git a/src/cmd/cgo/out.go b/src/cmd/cgo/out.go index dfa54e41d33399..622d35ac7b3bab 100644 --- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -1005,12 +1005,8 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) { } // Build the wrapper function compiled by gcc. - gccExport := "" - if goos == "windows" { - gccExport = "__declspec(dllexport) " - } var s strings.Builder - fmt.Fprintf(&s, "%s%s %s(", gccExport, gccResult, exp.ExpName) + fmt.Fprintf(&s, "%s %s(", gccResult, exp.ExpName) if fn.Recv != nil { s.WriteString(p.cgoType(fn.Recv.List[0].Type).C.String()) s.WriteString(" recv") diff --git a/src/cmd/link/internal/ld/lib.go b/src/cmd/link/internal/ld/lib.go index c7596d535e0ee5..79d3d37835e9ad 100644 --- a/src/cmd/link/internal/ld/lib.go +++ b/src/cmd/link/internal/ld/lib.go @@ -1772,7 +1772,8 @@ func (ctxt *Link) hostlink() { } // Force global symbols to be exported for dlopen, etc. - if ctxt.IsELF { + switch { + case ctxt.IsELF: if ctxt.DynlinkingGo() || ctxt.BuildMode == BuildModeCShared || !linkerFlagSupported(ctxt.Arch, argv[0], altLinker, "-Wl,--export-dynamic-symbol=main") { argv = append(argv, "-rdynamic") } else { @@ -1783,10 +1784,12 @@ func (ctxt *Link) hostlink() { sort.Strings(exports) argv = append(argv, exports...) } - } - if ctxt.HeadType == objabi.Haix { + case ctxt.IsAIX(): fileName := xcoffCreateExportFile(ctxt) argv = append(argv, "-Wl,-bE:"+fileName) + case ctxt.IsWindows() && !slices.Contains(flagExtldflags, "-Wl,--export-all-symbols"): + fileName := peCreateExportFile(ctxt, filepath.Base(outopt)) + argv = append(argv, fileName) } const unusedArguments = "-Qunused-arguments" diff --git a/src/cmd/link/internal/ld/pe.go b/src/cmd/link/internal/ld/pe.go index 5219a98dd47cf4..0f0650e5e149e3 100644 --- a/src/cmd/link/internal/ld/pe.go +++ b/src/cmd/link/internal/ld/pe.go @@ -8,6 +8,7 @@ package ld import ( + "bytes" "cmd/internal/objabi" "cmd/internal/sys" "cmd/link/internal/loader" @@ -17,6 +18,8 @@ import ( "fmt" "internal/buildcfg" "math" + "os" + "path/filepath" "slices" "sort" "strconv" @@ -1748,3 +1751,44 @@ func asmbPe(ctxt *Link) { pewrite(ctxt) } + +// peCreateExportFile creates a file with exported symbols for Windows .def files. +// ld will export all symbols, even those not marked for export, unless a .def file is provided. +func peCreateExportFile(ctxt *Link, libName string) (fname string) { + fname = filepath.Join(*flagTmpdir, "export_file.def") + var buf bytes.Buffer + + fmt.Fprintf(&buf, "LIBRARY %s\n", libName) + buf.WriteString("EXPORTS\n") + + ldr := ctxt.loader + var exports []string + for s := range ldr.ForAllCgoExportStatic() { + extname := ldr.SymExtname(s) + if !strings.HasPrefix(extname, "_cgoexp_") { + continue + } + if ldr.IsFileLocal(s) { + continue // Only export non-static symbols + } + // Retrieve the name of the initial symbol + // exported by cgo. + // The corresponding Go symbol is: + // _cgoexp_hashcode_symname. + name := strings.SplitN(extname, "_", 4)[3] + exports = append(exports, name) + } + if len(exports) == 0 { + // See runtime/cgo/windows.go for details. + exports = append(exports, "_cgo_stub_export") + } + sort.Strings(exports) + buf.WriteString(strings.Join(exports, "\n")) + + err := os.WriteFile(fname, buf.Bytes(), 0666) + if err != nil { + Errorf("WriteFile %s failed: %v", fname, err) + } + + return fname +} diff --git a/src/cmd/link/internal/ld/xcoff.go b/src/cmd/link/internal/ld/xcoff.go index 1bce2cf9b6124d..da728e25455618 100644 --- a/src/cmd/link/internal/ld/xcoff.go +++ b/src/cmd/link/internal/ld/xcoff.go @@ -1779,10 +1779,7 @@ func xcoffCreateExportFile(ctxt *Link) (fname string) { var buf bytes.Buffer ldr := ctxt.loader - for s, nsym := loader.Sym(1), loader.Sym(ldr.NSym()); s < nsym; s++ { - if !ldr.AttrCgoExport(s) { - continue - } + for s := range ldr.ForAllCgoExportStatic() { extname := ldr.SymExtname(s) if !strings.HasPrefix(extname, "._cgoexp_") { continue diff --git a/src/cmd/link/internal/loader/loader.go b/src/cmd/link/internal/loader/loader.go index 9f3ea3e553dd7c..103dad03001263 100644 --- a/src/cmd/link/internal/loader/loader.go +++ b/src/cmd/link/internal/loader/loader.go @@ -16,6 +16,7 @@ import ( "fmt" "internal/abi" "io" + "iter" "log" "math/bits" "os" @@ -1109,6 +1110,18 @@ func (l *Loader) SetAttrCgoExportStatic(i Sym, v bool) { } } +// ForAllCgoExportStatic returns an iterator over all symbols +// marked with the "cgo_export_static" compiler directive. +func (l *Loader) ForAllCgoExportStatic() iter.Seq[Sym] { + return func(yield func(Sym) bool) { + for s := range l.attrCgoExportStatic { + if !yield(s) { + break + } + } + } +} + // IsGeneratedSym returns true if a symbol's been previously marked as a // generator symbol through the SetIsGeneratedSym. The functions for generator // symbols are kept in the Link context. diff --git a/src/runtime/cgo/gcc_libinit_windows.c b/src/runtime/cgo/gcc_libinit_windows.c index 926f9168434638..7e7ff3e667266f 100644 --- a/src/runtime/cgo/gcc_libinit_windows.c +++ b/src/runtime/cgo/gcc_libinit_windows.c @@ -15,15 +15,6 @@ #include "libcgo.h" #include "libcgo_windows.h" -// Ensure there's one symbol marked __declspec(dllexport). -// If there are no exported symbols, the unfortunate behavior of -// the binutils linker is to also strip the relocations table, -// resulting in non-PIE binary. The other option is the -// --export-all-symbols flag, but we don't need to export all symbols -// and this may overflow the export table (#40795). -// See https://sourceware.org/bugzilla/show_bug.cgi?id=19011 -__declspec(dllexport) int _cgo_dummy_export; - static volatile LONG runtime_init_once_gate = 0; static volatile LONG runtime_init_once_done = 0; diff --git a/src/runtime/cgo/windows.go b/src/runtime/cgo/windows.go new file mode 100644 index 00000000000000..7ba61753dffda2 --- /dev/null +++ b/src/runtime/cgo/windows.go @@ -0,0 +1,22 @@ +// 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. + +//go:build windows + +package cgo + +import _ "unsafe" // for go:linkname + +// _cgo_stub_export is only used to ensure there's at least one symbol +// in the .def file passed to the external linker. +// If there are no exported symbols, the unfortunate behavior of +// the binutils linker is to also strip the relocations table, +// resulting in non-PIE binary. The other option is the +// --export-all-symbols flag, but we don't need to export all symbols +// and this may overflow the export table (#40795). +// See https://sourceware.org/bugzilla/show_bug.cgi?id=19011 +// +//go:cgo_export_static _cgo_stub_export +//go:linkname _cgo_stub_export _cgo_stub_export +var _cgo_stub_export uintptr From 690fc2fb05e720850a474c72bf3a8a9a6638cef7 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 16 Sep 2025 10:25:58 +0200 Subject: [PATCH 10/63] internal/poll: remove buf field from operation WSASend and WSARecv functions capture the WSABuf structure before returning, so there is no need to keep a copy of it in the operation structure. Write and Read functions don't need it, they can operate directly on the byte slice. To be on the safe side, pin the input byte slice so that stack-allocated slices don't get moved while overlapped I/O is in progress. Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-longtest,gotip-windows-amd64-race Change-Id: I474bed94e11acafa0bdd8392b5dcf8993d8e1ed5 Reviewed-on: https://go-review.googlesource.com/c/go/+/704155 Reviewed-by: Junyang Shao LUCI-TryBot-Result: Go LUCI Reviewed-by: Damien Neil --- src/internal/poll/fd_windows.go | 170 ++++++++++++++++++-------------- src/net/tcpsock_test.go | 7 +- 2 files changed, 100 insertions(+), 77 deletions(-) diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go index dd9845d1b223c3..9323e49eb745d7 100644 --- a/src/internal/poll/fd_windows.go +++ b/src/internal/poll/fd_windows.go @@ -9,6 +9,7 @@ import ( "internal/race" "internal/syscall/windows" "io" + "runtime" "sync" "sync/atomic" "syscall" @@ -75,9 +76,6 @@ type operation struct { // fields used by runtime.netpoll runtimeCtx uintptr mode int32 - - // fields used only by net package - buf syscall.WSABuf } func (o *operation) setEvent() { @@ -107,9 +105,8 @@ func (fd *FD) overlapped(o *operation) *syscall.Overlapped { return &o.o } -func (o *operation) InitBuf(buf []byte) { - o.buf.Len = uint32(len(buf)) - o.buf.Buf = unsafe.SliceData(buf) +func newWsaBuf(b []byte) *syscall.WSABuf { + return &syscall.WSABuf{Buf: unsafe.SliceData(b), Len: uint32(len(b))} } var wsaBufsPool = sync.Pool{ @@ -362,6 +359,9 @@ type FD struct { isBlocking bool disassociated atomic.Bool + + readPinner runtime.Pinner + writePinner runtime.Pinner } // setOffset sets the offset fields of the overlapped object @@ -537,6 +537,11 @@ func (fd *FD) Read(buf []byte) (int, error) { defer fd.readUnlock() } + if len(buf) > 0 && !fd.isBlocking { + fd.readPinner.Pin(&buf[0]) + defer fd.readPinner.Unpin() + } + if len(buf) > maxRW { buf = buf[:maxRW] } @@ -547,10 +552,8 @@ func (fd *FD) Read(buf []byte) (int, error) { case kindConsole: n, err = fd.readConsole(buf) case kindFile, kindPipe: - o := &fd.rop - o.InitBuf(buf) - n, err = fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.ReadFile(fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &qty, fd.overlapped(o)) + n, err = fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + err = syscall.ReadFile(fd.Sysfd, buf, &qty, fd.overlapped(o)) return qty, err }) fd.addOffset(n) @@ -564,11 +567,9 @@ func (fd *FD) Read(buf []byte) (int, error) { } } case kindNet: - o := &fd.rop - o.InitBuf(buf) - n, err = fd.execIO(o, func(o *operation) (qty uint32, err error) { + n, err = fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { var flags uint32 - err = syscall.WSARecv(fd.Sysfd, &o.buf, 1, &qty, &flags, &o.o, nil) + err = syscall.WSARecv(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &o.o, nil) return qty, err }) if race.Enabled { @@ -656,7 +657,7 @@ func (fd *FD) readConsole(b []byte) (int, error) { } // Pread emulates the Unix pread system call. -func (fd *FD) Pread(b []byte, off int64) (int, error) { +func (fd *FD) Pread(buf []byte, off int64) (int, error) { if fd.kind == kindPipe { // Pread does not work with pipes return 0, syscall.ESPIPE @@ -667,8 +668,13 @@ func (fd *FD) Pread(b []byte, off int64) (int, error) { } defer fd.readWriteUnlock() - if len(b) > maxRW { - b = b[:maxRW] + if len(buf) > 0 && !fd.isBlocking { + fd.readPinner.Pin(&buf[0]) + defer fd.readPinner.Unpin() + } + + if len(buf) > maxRW { + buf = buf[:maxRW] } if fd.isBlocking { @@ -687,17 +693,15 @@ func (fd *FD) Pread(b []byte, off int64) (int, error) { curoffset := fd.offset defer fd.setOffset(curoffset) } - o := &fd.rop - o.InitBuf(b) fd.setOffset(off) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.ReadFile(fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &qty, &o.o) + n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + err = syscall.ReadFile(fd.Sysfd, buf, &qty, &o.o) return qty, err }) if err == syscall.ERROR_HANDLE_EOF { err = io.EOF } - if len(b) != 0 { + if len(buf) != 0 { err = fd.eofError(n, err) } return n, err @@ -715,14 +719,18 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) { return 0, nil, err } defer fd.readUnlock() - o := &fd.rop - o.InitBuf(buf) + + if !fd.isBlocking { + fd.readPinner.Pin(&buf[0]) + defer fd.readPinner.Unpin() + } + rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { rsan := int32(unsafe.Sizeof(*rsa)) var flags uint32 - err = syscall.WSARecvFrom(fd.Sysfd, &o.buf, 1, &qty, &flags, rsa, &rsan, &o.o, nil) + err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil) return qty, err }) err = fd.eofError(n, err) @@ -745,14 +753,18 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) return 0, err } defer fd.readUnlock() - o := &fd.rop - o.InitBuf(buf) + + if !fd.isBlocking { + fd.readPinner.Pin(&buf[0]) + defer fd.readPinner.Unpin() + } + rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { rsan := int32(unsafe.Sizeof(*rsa)) var flags uint32 - err = syscall.WSARecvFrom(fd.Sysfd, &o.buf, 1, &qty, &flags, rsa, &rsan, &o.o, nil) + err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil) return qty, err }) err = fd.eofError(n, err) @@ -775,14 +787,18 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) return 0, err } defer fd.readUnlock() - o := &fd.rop - o.InitBuf(buf) + + if !fd.isBlocking { + fd.readPinner.Pin(&buf[0]) + defer fd.readPinner.Unpin() + } + rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { rsan := int32(unsafe.Sizeof(*rsa)) var flags uint32 - err = syscall.WSARecvFrom(fd.Sysfd, &o.buf, 1, &qty, &flags, rsa, &rsan, &o.o, nil) + err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil) return qty, err }) err = fd.eofError(n, err) @@ -807,6 +823,11 @@ func (fd *FD) Write(buf []byte) (int, error) { defer fd.writeUnlock() } + if len(buf) > 0 && !fd.isBlocking { + fd.writePinner.Pin(&buf[0]) + defer fd.writePinner.Unpin() + } + var ntotal int for { max := len(buf) @@ -820,10 +841,8 @@ func (fd *FD) Write(buf []byte) (int, error) { case kindConsole: n, err = fd.writeConsole(b) case kindPipe, kindFile: - o := &fd.wop - o.InitBuf(b) - n, err = fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.WriteFile(fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &qty, fd.overlapped(o)) + n, err = fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = syscall.WriteFile(fd.Sysfd, b, &qty, fd.overlapped(o)) return qty, err }) fd.addOffset(n) @@ -831,10 +850,8 @@ func (fd *FD) Write(buf []byte) (int, error) { if race.Enabled { race.ReleaseMerge(unsafe.Pointer(&ioSync)) } - o := &fd.wop - o.InitBuf(b) - n, err = fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.WSASend(fd.Sysfd, &o.buf, 1, &qty, 0, &o.o, nil) + n, err = fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = syscall.WSASend(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, &o.o, nil) return qty, err }) } @@ -903,6 +920,11 @@ func (fd *FD) Pwrite(buf []byte, off int64) (int, error) { } defer fd.readWriteUnlock() + if len(buf) > 0 && !fd.isBlocking { + fd.writePinner.Pin(&buf[0]) + defer fd.writePinner.Unpin() + } + if fd.isBlocking { curoffset, err := syscall.Seek(fd.Sysfd, 0, io.SeekCurrent) if err != nil { @@ -926,12 +948,9 @@ func (fd *FD) Pwrite(buf []byte, off int64) (int, error) { if max-ntotal > maxRW { max = ntotal + maxRW } - b := buf[ntotal:max] - o := &fd.wop - o.InitBuf(b) fd.setOffset(off + int64(ntotal)) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.WriteFile(fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &qty, &o.o) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = syscall.WriteFile(fd.Sysfd, buf[ntotal:max], &qty, &o.o) return qty, err }) if n > 0 { @@ -978,25 +997,26 @@ func (fd *FD) WriteTo(buf []byte, sa syscall.Sockaddr) (int, error) { if len(buf) == 0 { // handle zero-byte payload - o := &fd.wop - o.InitBuf(buf) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.WSASendto(fd.Sysfd, &o.buf, 1, &qty, 0, sa, &o.o, nil) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = syscall.WSASendto(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, 0, sa, &o.o, nil) return qty, err }) return n, err } + if !fd.isBlocking { + fd.writePinner.Pin(&buf[0]) + defer fd.writePinner.Unpin() + } + ntotal := 0 for len(buf) > 0 { b := buf if len(b) > maxRW { b = b[:maxRW] } - o := &fd.wop - o.InitBuf(b) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = syscall.WSASendto(fd.Sysfd, &o.buf, 1, &qty, 0, sa, &o.o, nil) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = syscall.WSASendto(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, sa, &o.o, nil) return qty, err }) ntotal += int(n) @@ -1017,25 +1037,26 @@ func (fd *FD) WriteToInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) if len(buf) == 0 { // handle zero-byte payload - o := &fd.wop - o.InitBuf(buf) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = windows.WSASendtoInet4(fd.Sysfd, &o.buf, 1, &qty, 0, sa4, &o.o, nil) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = windows.WSASendtoInet4(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, 0, sa4, &o.o, nil) return qty, err }) return n, err } + if !fd.isBlocking { + fd.writePinner.Pin(&buf[0]) + defer fd.writePinner.Unpin() + } + ntotal := 0 for len(buf) > 0 { b := buf if len(b) > maxRW { b = b[:maxRW] } - o := &fd.wop - o.InitBuf(b) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = windows.WSASendtoInet4(fd.Sysfd, &o.buf, 1, &qty, 0, sa4, &o.o, nil) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = windows.WSASendtoInet4(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, sa4, &o.o, nil) return qty, err }) ntotal += int(n) @@ -1056,25 +1077,26 @@ func (fd *FD) WriteToInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) if len(buf) == 0 { // handle zero-byte payload - o := &fd.wop - o.InitBuf(buf) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = windows.WSASendtoInet6(fd.Sysfd, &o.buf, 1, &qty, 0, sa6, &o.o, nil) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = windows.WSASendtoInet6(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, 0, sa6, &o.o, nil) return qty, err }) return n, err } + if !fd.isBlocking { + fd.writePinner.Pin(&buf[0]) + defer fd.writePinner.Unpin() + } + ntotal := 0 for len(buf) > 0 { b := buf if len(b) > maxRW { b = b[:maxRW] } - o := &fd.wop - o.InitBuf(b) - n, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { - err = windows.WSASendtoInet6(fd.Sysfd, &o.buf, 1, &qty, 0, sa6, &o.o, nil) + n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + err = windows.WSASendtoInet6(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, sa6, &o.o, nil) return qty, err }) ntotal += int(n) @@ -1264,14 +1286,12 @@ func (fd *FD) RawRead(f func(uintptr) bool) error { // Use a zero-byte read as a way to get notified when this // socket is readable. h/t https://stackoverflow.com/a/42019668/332798 - o := &fd.rop - o.InitBuf(nil) - _, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { + _, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { var flags uint32 if !fd.IsStream { flags |= windows.MSG_PEEK } - err = syscall.WSARecv(fd.Sysfd, &o.buf, 1, &qty, &flags, &o.o, nil) + err = syscall.WSARecv(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, &flags, &o.o, nil) return qty, err }) if err == windows.WSAEMSGSIZE { diff --git a/src/net/tcpsock_test.go b/src/net/tcpsock_test.go index 9ed49a925b4b39..085989c7499b60 100644 --- a/src/net/tcpsock_test.go +++ b/src/net/tcpsock_test.go @@ -475,6 +475,9 @@ func TestTCPReadWriteAllocs(t *testing.T) { t.Skipf("not supported on %s", runtime.GOOS) } + // Optimizations are required to remove the allocs. + testenv.SkipIfOptimizationOff(t) + ln, err := Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) @@ -509,7 +512,7 @@ func TestTCPReadWriteAllocs(t *testing.T) { } }) if allocs > 0 { - t.Fatalf("got %v; want 0", allocs) + t.Errorf("got %v; want 0", allocs) } var bufwrt [128]byte @@ -531,7 +534,7 @@ func TestTCPReadWriteAllocs(t *testing.T) { } }) if allocs > 0 { - t.Fatalf("got %v; want 0", allocs) + t.Errorf("got %v; want 0", allocs) } } From db3cb3fd9a09355a2f239dcb28c480b18bfa7f5e Mon Sep 17 00:00:00 2001 From: Ian Lance Taylor Date: Sun, 28 Sep 2025 21:22:09 -0700 Subject: [PATCH 11/63] runtime: correct reference to getStackMap in comment Change-Id: I9b1fa390434dbda7d49a36b0114c68f942c11d3f Reviewed-on: https://go-review.googlesource.com/c/go/+/707575 Auto-Submit: Ian Lance Taylor Reviewed-by: Michael Knyszek Reviewed-by: Carlos Amedee LUCI-TryBot-Result: Go LUCI --- src/runtime/traceback.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/traceback.go b/src/runtime/traceback.go index 949d48c79a6df5..53f41bca0b11ff 100644 --- a/src/runtime/traceback.go +++ b/src/runtime/traceback.go @@ -429,7 +429,7 @@ func (u *unwinder) resolveInternal(innermost, isSyscall bool) { // gp._defer for a defer corresponding to this function, but that // is hard to do with defer records on the stack during a stack copy.) // Note: the +1 is to offset the -1 that - // stack.go:getStackMap does to back up a return + // (*stkframe).getStackMap does to back up a return // address make sure the pc is in the CALL instruction. } else { frame.continpc = 0 From db4fade7599d49dc85a7ef670499be0ccd62c58e Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Mon, 22 Sep 2025 14:05:23 +0200 Subject: [PATCH 12/63] crypto/internal/fips140/mlkem: make CAST conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It taks north of 130µs on my machine, which is enough to be worth shaving off at init time. Change-Id: I6a6a696463de228bc3e7b9ca10c12ddbaabdf384 Reviewed-on: https://go-review.googlesource.com/c/go/+/707695 Auto-Submit: Filippo Valsorda Reviewed-by: Daniel McCarney Reviewed-by: Roland Shoemaker LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- src/crypto/internal/fips140/mlkem/cast.go | 13 ++++++------ .../internal/fips140/mlkem/mlkem1024.go | 5 +++++ src/crypto/internal/fips140/mlkem/mlkem768.go | 5 +++++ src/crypto/internal/fips140test/cast_test.go | 21 ++++++++++++++++--- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/crypto/internal/fips140/mlkem/cast.go b/src/crypto/internal/fips140/mlkem/cast.go index a432d1fdab0e2b..ea089c1b76c0c0 100644 --- a/src/crypto/internal/fips140/mlkem/cast.go +++ b/src/crypto/internal/fips140/mlkem/cast.go @@ -9,9 +9,10 @@ import ( "crypto/internal/fips140" _ "crypto/internal/fips140/check" "errors" + "sync" ) -func init() { +var fipsSelfTest = sync.OnceFunc(func() { fips140.CAST("ML-KEM-768", func() error { var d = &[32]byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, @@ -40,14 +41,12 @@ func init() { dk := &DecapsulationKey768{} kemKeyGen(dk, d, z) ek := dk.EncapsulationKey() - Ke, c := ek.EncapsulateInternal(m) - Kd, err := dk.Decapsulate(c) - if err != nil { - return err - } + var cc [CiphertextSize768]byte + Ke, _ := kemEncaps(&cc, ek, m) + Kd := kemDecaps(dk, &cc) if !bytes.Equal(Ke, K) || !bytes.Equal(Kd, K) { return errors.New("unexpected result") } return nil }) -} +}) diff --git a/src/crypto/internal/fips140/mlkem/mlkem1024.go b/src/crypto/internal/fips140/mlkem/mlkem1024.go index 1419cf20fa9c67..edde161422cb6f 100644 --- a/src/crypto/internal/fips140/mlkem/mlkem1024.go +++ b/src/crypto/internal/fips140/mlkem/mlkem1024.go @@ -113,6 +113,7 @@ func GenerateKey1024() (*DecapsulationKey1024, error) { } func generateKey1024(dk *DecapsulationKey1024) (*DecapsulationKey1024, error) { + fipsSelfTest() var d [32]byte drbg.Read(d[:]) var z [32]byte @@ -126,6 +127,7 @@ func generateKey1024(dk *DecapsulationKey1024) (*DecapsulationKey1024, error) { // GenerateKeyInternal1024 is a derandomized version of GenerateKey1024, // exclusively for use in tests. func GenerateKeyInternal1024(d, z *[32]byte) *DecapsulationKey1024 { + fipsSelfTest() dk := &DecapsulationKey1024{} kemKeyGen1024(dk, d, z) return dk @@ -278,6 +280,7 @@ func (ek *EncapsulationKey1024) Encapsulate() (sharedKey, ciphertext []byte) { } func (ek *EncapsulationKey1024) encapsulate(cc *[CiphertextSize1024]byte) (sharedKey, ciphertext []byte) { + fipsSelfTest() var m [messageSize]byte drbg.Read(m[:]) // Note that the modulus check (step 2 of the encapsulation key check from @@ -289,6 +292,7 @@ func (ek *EncapsulationKey1024) encapsulate(cc *[CiphertextSize1024]byte) (share // EncapsulateInternal is a derandomized version of Encapsulate, exclusively for // use in tests. func (ek *EncapsulationKey1024) EncapsulateInternal(m *[32]byte) (sharedKey, ciphertext []byte) { + fipsSelfTest() cc := &[CiphertextSize1024]byte{} return kemEncaps1024(cc, ek, m) } @@ -394,6 +398,7 @@ func pkeEncrypt1024(cc *[CiphertextSize1024]byte, ex *encryptionKey1024, m *[mes // // The shared key must be kept secret. func (dk *DecapsulationKey1024) Decapsulate(ciphertext []byte) (sharedKey []byte, err error) { + fipsSelfTest() if len(ciphertext) != CiphertextSize1024 { return nil, errors.New("mlkem: invalid ciphertext length") } diff --git a/src/crypto/internal/fips140/mlkem/mlkem768.go b/src/crypto/internal/fips140/mlkem/mlkem768.go index 298660e4e977dd..088c2954de6a5c 100644 --- a/src/crypto/internal/fips140/mlkem/mlkem768.go +++ b/src/crypto/internal/fips140/mlkem/mlkem768.go @@ -172,6 +172,7 @@ func GenerateKey768() (*DecapsulationKey768, error) { } func generateKey(dk *DecapsulationKey768) (*DecapsulationKey768, error) { + fipsSelfTest() var d [32]byte drbg.Read(d[:]) var z [32]byte @@ -185,6 +186,7 @@ func generateKey(dk *DecapsulationKey768) (*DecapsulationKey768, error) { // GenerateKeyInternal768 is a derandomized version of GenerateKey768, // exclusively for use in tests. func GenerateKeyInternal768(d, z *[32]byte) *DecapsulationKey768 { + fipsSelfTest() dk := &DecapsulationKey768{} kemKeyGen(dk, d, z) return dk @@ -337,6 +339,7 @@ func (ek *EncapsulationKey768) Encapsulate() (sharedKey, ciphertext []byte) { } func (ek *EncapsulationKey768) encapsulate(cc *[CiphertextSize768]byte) (sharedKey, ciphertext []byte) { + fipsSelfTest() var m [messageSize]byte drbg.Read(m[:]) // Note that the modulus check (step 2 of the encapsulation key check from @@ -348,6 +351,7 @@ func (ek *EncapsulationKey768) encapsulate(cc *[CiphertextSize768]byte) (sharedK // EncapsulateInternal is a derandomized version of Encapsulate, exclusively for // use in tests. func (ek *EncapsulationKey768) EncapsulateInternal(m *[32]byte) (sharedKey, ciphertext []byte) { + fipsSelfTest() cc := &[CiphertextSize768]byte{} return kemEncaps(cc, ek, m) } @@ -453,6 +457,7 @@ func pkeEncrypt(cc *[CiphertextSize768]byte, ex *encryptionKey, m *[messageSize] // // The shared key must be kept secret. func (dk *DecapsulationKey768) Decapsulate(ciphertext []byte) (sharedKey []byte, err error) { + fipsSelfTest() if len(ciphertext) != CiphertextSize768 { return nil, errors.New("mlkem: invalid ciphertext length") } diff --git a/src/crypto/internal/fips140test/cast_test.go b/src/crypto/internal/fips140test/cast_test.go index b043a71f04effa..5bbc964b617b2b 100644 --- a/src/crypto/internal/fips140test/cast_test.go +++ b/src/crypto/internal/fips140test/cast_test.go @@ -48,8 +48,8 @@ var allCASTs = []string{ "HKDF-SHA2-256", "HMAC-SHA2-256", "KAS-ECC-SSC P-256", - "ML-KEM PCT", - "ML-KEM PCT", + "ML-KEM PCT", // -768 + "ML-KEM PCT", // -1024 "ML-KEM-768", "PBKDF2", "RSA sign and verify PCT", @@ -104,29 +104,44 @@ func TestAllCASTs(t *testing.T) { // TestConditionals causes the conditional CASTs and PCTs to be invoked. func TestConditionals(t *testing.T) { - mlkem.GenerateKey768() + // ML-KEM PCT + kMLKEM, err := mlkem.GenerateKey768() + if err != nil { + t.Error(err) + } else { + // ML-KEM-768 + kMLKEM.EncapsulationKey().Encapsulate() + } + // ECDH PCT kDH, err := ecdh.GenerateKey(ecdh.P256(), rand.Reader) if err != nil { t.Error(err) } else { + // KAS-ECC-SSC P-256 ecdh.ECDH(ecdh.P256(), kDH, kDH.PublicKey()) } + // ECDSA PCT kDSA, err := ecdsa.GenerateKey(ecdsa.P256(), rand.Reader) if err != nil { t.Error(err) } else { + // ECDSA P-256 SHA2-512 sign and verify ecdsa.SignDeterministic(ecdsa.P256(), sha256.New, kDSA, make([]byte, 32)) } + // Ed25519 sign and verify PCT k25519, err := ed25519.GenerateKey() if err != nil { t.Error(err) } else { + // Ed25519 sign and verify ed25519.Sign(k25519, make([]byte, 32)) } + // RSA sign and verify PCT kRSA, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Error(err) } else { + // RSASSA-PKCS-v1.5 2048-bit sign and verify rsa.SignPKCS1v15(kRSA, crypto.SHA256.String(), make([]byte, 32)) } t.Log("completed successfully") From fc88e18b4a781a8751799a123cdac8b29a92409d Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Thu, 11 Sep 2025 21:04:05 +0200 Subject: [PATCH 13/63] crypto/internal/fips140/entropy: add CPU jitter-based entropy source Heavily inspired by the BoringSSL implementation. Change-Id: I6a6a6964b22826d54700c8b3d555054163cef5fe Co-authored-by: Daniel Morsing Cq-Include-Trybots: luci.golang.try:gotip-linux-s390x,gotip-linux-ppc64_power10,gotip-linux-ppc64le_power10,gotip-linux-ppc64le_power8,gotip-linux-arm,gotip-darwin-arm64_15,gotip-windows-arm64,gotip-freebsd-amd64 Reviewed-on: https://go-review.googlesource.com/c/go/+/703015 LUCI-TryBot-Result: Go LUCI Reviewed-by: Roland Shoemaker Reviewed-by: Daniel McCarney Auto-Submit: Filippo Valsorda Reviewed-by: Cherry Mui --- src/crypto/internal/entropy/entropy.go | 6 +- src/crypto/internal/fips140/drbg/rand.go | 61 +++- .../internal/fips140/entropy/entropy.go | 202 ++++++++++++++ src/crypto/internal/fips140/entropy/sha384.go | 191 +++++++++++++ src/crypto/internal/fips140/fips140.go | 2 + .../internal/fips140deps/fipsdeps_test.go | 4 +- src/crypto/internal/fips140deps/time/time.go | 21 ++ .../internal/fips140deps/time/time_windows.go | 17 ++ .../internal/fips140test/entropy_test.go | 264 ++++++++++++++++++ src/go/build/deps_test.go | 5 + 10 files changed, 758 insertions(+), 15 deletions(-) create mode 100644 src/crypto/internal/fips140/entropy/entropy.go create mode 100644 src/crypto/internal/fips140/entropy/sha384.go create mode 100644 src/crypto/internal/fips140deps/time/time.go create mode 100644 src/crypto/internal/fips140deps/time/time_windows.go create mode 100644 src/crypto/internal/fips140test/entropy_test.go diff --git a/src/crypto/internal/entropy/entropy.go b/src/crypto/internal/entropy/entropy.go index 5319e9e47a7455..73fd5298007a11 100644 --- a/src/crypto/internal/entropy/entropy.go +++ b/src/crypto/internal/entropy/entropy.go @@ -3,9 +3,11 @@ // license that can be found in the LICENSE file. // Package entropy provides the passive entropy source for the FIPS 140-3 -// module. It is only used in FIPS mode by [crypto/internal/fips140/drbg.Read]. +// module. It is only used in FIPS mode by [crypto/internal/fips140/drbg.Read] +// from the FIPS 140-3 Go Cryptographic Module v1.0.0. Later versions of the +// module have an internal CPU jitter-based entropy source. // -// This complies with IG 9.3.A, Additional Comment 12, which until January 1, +// This complied with IG 9.3.A, Additional Comment 12, which until January 1, // 2026 allows new modules to meet an [earlier version] of Resolution 2(b): // "A software module that contains an approved DRBG that receives a LOAD // command (or its logical equivalent) with entropy obtained from [...] inside diff --git a/src/crypto/internal/fips140/drbg/rand.go b/src/crypto/internal/fips140/drbg/rand.go index c1a3ea0ae658ff..3ccb018e326047 100644 --- a/src/crypto/internal/fips140/drbg/rand.go +++ b/src/crypto/internal/fips140/drbg/rand.go @@ -9,21 +9,53 @@ package drbg import ( - "crypto/internal/entropy" "crypto/internal/fips140" + "crypto/internal/fips140/entropy" "crypto/internal/randutil" "crypto/internal/sysrand" "io" "sync" + "sync/atomic" ) -var drbgs = sync.Pool{ +// 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 { - var c *Counter - entropy.Depleted(func(seed *[48]byte) { - c = NewCounter(seed) - }) - return c + return NewCounter(getEntropy()) }, } @@ -44,8 +76,15 @@ func Read(b []byte) { additionalInput := new([SeedSize]byte) sysrand.Read(additionalInput[:16]) - drbg := drbgs.Get().(*Counter) - defer drbgs.Put(drbg) + 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) @@ -54,9 +93,7 @@ func Read(b []byte) { // 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. - entropy.Depleted(func(seed *[48]byte) { - drbg.Reseed(seed, additionalInput) - }) + drbg.Reseed(getEntropy(), additionalInput) additionalInput = nil continue } diff --git a/src/crypto/internal/fips140/entropy/entropy.go b/src/crypto/internal/fips140/entropy/entropy.go new file mode 100644 index 00000000000000..273f05c817aff8 --- /dev/null +++ b/src/crypto/internal/fips140/entropy/entropy.go @@ -0,0 +1,202 @@ +// 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 entropy implements a CPU jitter-based SP 800-90B entropy source. +package entropy + +import ( + "crypto/internal/fips140deps/time" + "errors" + "sync/atomic" + "unsafe" +) + +// Version returns the version of the entropy source. +// +// This is independent of the FIPS 140-3 module version, in order to reuse the +// ESV certificate across module versions. +func Version() string { + return "v1.0.0" +} + +// ScratchBuffer is a large buffer that will be written to using atomics, to +// generate noise from memory access timings. Its contents do not matter. +type ScratchBuffer [1 << 25]byte + +// Seed returns a 384-bit seed with full entropy. +// +// memory is passed in to allow changing the allocation strategy without +// modifying the frozen and certified entropy source in this package. +// +// Seed returns an error if the entropy source startup health tests fail, which +// has a non-negligible chance of happening. +func Seed(memory *ScratchBuffer) ([48]byte, error) { + // Collect w = 1024 samples, each certified to provide no less than h = 0.5 + // bits of entropy, for a total of hᵢₙ = w × h = 512 bits of entropy, over + // nᵢₙ = w × n = 8192 bits of input data. + var samples [1024]byte + if err := Samples(samples[:], memory); err != nil { + return [48]byte{}, err + } + + // Use a vetted unkeyed conditioning component, SHA-384, with nw = 384 and + // nₒᵤₜ = 384. Per the formula in SP 800-90B Section 3.1.5.1.2, the output + // entropy hₒᵤₜ is: + // + // sage: n_in = 8192 + // sage: n_out = 384 + // sage: nw = 384 + // sage: h_in = 512 + // sage: P_high = 2^(-h_in) + // sage: P_low = (1 - P_high) / (2^n_in - 1) + // sage: n = min(n_out, nw) + // sage: ψ = 2^(n_in - n) * P_low + P_high + // sage: U = 2^(n_in - n) + sqrt(2 * n * 2^(n_in - n) * ln(2)) + // sage: ω = U * P_low + // sage: h_out = -log(max(ψ, ω), 2) + // sage: h_out.n() + // 384.000000000000 + // + // According to Implementation Guidance D.K, Resolution 19, since + // + // - the conditioning component is vetted, + // - hᵢₙ = 512 ≥ nₒᵤₜ + 64 = 448, and + // - nₒᵤₜ ≤ security strength of SHA-384 = 384 (per SP 800-107 Rev. 1, Table 1), + // + // we can claim the output has full entropy. + return SHA384(&samples), nil +} + +// Samples starts a new entropy source, collects the requested number of +// samples, conducts startup health tests, and returns the samples or an error +// if the health tests fail. +// +// The health tests have a non-negligible chance of failing. +func Samples(samples []uint8, memory *ScratchBuffer) error { + if len(samples) < 1024 { + return errors.New("entropy: at least 1024 samples are required for startup health tests") + } + s := newSource(memory) + for range 4 { + // Warm up the source to avoid any initial bias. + _ = s.Sample() + } + for i := range samples { + samples[i] = s.Sample() + } + if err := RepetitionCountTest(samples); err != nil { + return err + } + if err := AdaptiveProportionTest(samples); err != nil { + return err + } + return nil +} + +type source struct { + memory *ScratchBuffer + lcgState uint32 + previous int64 +} + +func newSource(memory *ScratchBuffer) *source { + return &source{ + memory: memory, + lcgState: uint32(time.HighPrecisionNow()), + previous: time.HighPrecisionNow(), + } +} + +// touchMemory performs a write to memory at the given index. +// +// The memory slice is passed in and may be shared across sources e.g. to avoid +// the significant (~500µs) cost of zeroing a new allocation on every [Seed] call. +func touchMemory(memory *ScratchBuffer, idx uint32) { + idx = idx / 4 * 4 // align to 32 bits + u32 := (*uint32)(unsafe.Pointer(&memory[idx])) + last := atomic.LoadUint32(u32) + atomic.SwapUint32(u32, last+13) +} + +func (s *source) Sample() uint8 { + // Perform a few memory accesses in an unpredictable pattern to expose the + // next measurement to as much system noise as possible. + memory, lcgState := s.memory, s.lcgState + _ = memory[0] // hoist the nil check out of touchMemory + for range 64 { + lcgState = 1664525*lcgState + 1013904223 + // Discard the lower bits, which tend to fall into short cycles. + idx := (lcgState >> 6) & (1<<25 - 1) + touchMemory(memory, idx) + } + s.lcgState = lcgState + + t := time.HighPrecisionNow() + sample := t - s.previous + s.previous = t + + // Reduce the symbol space to 256 values, assuming most of the entropy is in + // the least-significant bits, which represent the highest-resolution timing + // differences. + return uint8(sample) +} + +// RepetitionCountTest implements the repetition count test from SP 800-90B +// Section 4.4.1. It returns an error if any symbol is repeated C = 41 or more +// times in a row. +// +// This C value is calculated from a target failure probability α = 2⁻²⁰ and a +// claimed min-entropy per symbol h = 0.5 bits, using the formula in SP 800-90B +// Section 4.4.1. +// +// sage: α = 2^-20 +// sage: H = 0.5 +// sage: 1 + ceil(-log(α, 2) / H) +// 41 +func RepetitionCountTest(samples []uint8) error { + x := samples[0] + count := 1 + for _, y := range samples[1:] { + if y == x { + count++ + if count >= 41 { + return errors.New("entropy: repetition count health test failed") + } + } else { + x = y + count = 1 + } + } + return nil +} + +// AdaptiveProportionTest implements the adaptive proportion test from SP 800-90B +// Section 4.4.2. It returns an error if any symbol appears C = 410 or more +// times in the last W = 512 samples. +// +// This C value is calculated from a target failure probability α = 2⁻²⁰, a +// window size W = 512, and a claimed min-entropy per symbol h = 0.5 bits, using +// the formula in SP 800-90B Section 4.4.2, equivalent to the Microsoft Excel +// formula 1+CRITBINOM(W, power(2,(−H)),1−α). +// +// sage: from scipy.stats import binom +// sage: α = 2^-20 +// sage: H = 0.5 +// sage: W = 512 +// sage: C = 1 + binom.ppf(1 - α, W, 2**(-H)) +// sage: ceil(C) +// 410 +func AdaptiveProportionTest(samples []uint8) error { + var counts [256]int + for i, x := range samples { + counts[x]++ + if i >= 512 { + counts[samples[i-512]]-- + } + if counts[x] >= 410 { + return errors.New("entropy: adaptive proportion health test failed") + } + } + return nil +} diff --git a/src/crypto/internal/fips140/entropy/sha384.go b/src/crypto/internal/fips140/entropy/sha384.go new file mode 100644 index 00000000000000..ec23cfc9ad3661 --- /dev/null +++ b/src/crypto/internal/fips140/entropy/sha384.go @@ -0,0 +1,191 @@ +// 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 entropy + +import "math/bits" + +// This file includes a SHA-384 implementation to insulate the entropy source +// from any changes in the FIPS 140-3 module's crypto/internal/fips140/sha512 +// package. We only support 1024-byte inputs. + +func SHA384(p *[1024]byte) [48]byte { + h := [8]uint64{ + 0xcbbb9d5dc1059ed8, + 0x629a292a367cd507, + 0x9159015a3070dd17, + 0x152fecd8f70e5939, + 0x67332667ffc00b31, + 0x8eb44a8768581511, + 0xdb0c2e0d64f98fa7, + 0x47b5481dbefa4fa4, + } + + sha384Block(&h, (*[128]byte)(p[0:128])) + sha384Block(&h, (*[128]byte)(p[128:256])) + sha384Block(&h, (*[128]byte)(p[256:384])) + sha384Block(&h, (*[128]byte)(p[384:512])) + sha384Block(&h, (*[128]byte)(p[512:640])) + sha384Block(&h, (*[128]byte)(p[640:768])) + sha384Block(&h, (*[128]byte)(p[768:896])) + sha384Block(&h, (*[128]byte)(p[896:1024])) + + var padlen [128]byte + padlen[0] = 0x80 + bePutUint64(padlen[112+8:], 1024*8) + sha384Block(&h, &padlen) + + var digest [48]byte + bePutUint64(digest[0:], h[0]) + bePutUint64(digest[8:], h[1]) + bePutUint64(digest[16:], h[2]) + bePutUint64(digest[24:], h[3]) + bePutUint64(digest[32:], h[4]) + bePutUint64(digest[40:], h[5]) + return digest +} + +var _K = [...]uint64{ + 0x428a2f98d728ae22, + 0x7137449123ef65cd, + 0xb5c0fbcfec4d3b2f, + 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, + 0x59f111f1b605d019, + 0x923f82a4af194f9b, + 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, + 0x12835b0145706fbe, + 0x243185be4ee4b28c, + 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, + 0x80deb1fe3b1696b1, + 0x9bdc06a725c71235, + 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, + 0xefbe4786384f25e3, + 0x0fc19dc68b8cd5b5, + 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, + 0x4a7484aa6ea6e483, + 0x5cb0a9dcbd41fbd4, + 0x76f988da831153b5, + 0x983e5152ee66dfab, + 0xa831c66d2db43210, + 0xb00327c898fb213f, + 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, + 0xd5a79147930aa725, + 0x06ca6351e003826f, + 0x142929670a0e6e70, + 0x27b70a8546d22ffc, + 0x2e1b21385c26c926, + 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, + 0x650a73548baf63de, + 0x766a0abb3c77b2a8, + 0x81c2c92e47edaee6, + 0x92722c851482353b, + 0xa2bfe8a14cf10364, + 0xa81a664bbc423001, + 0xc24b8b70d0f89791, + 0xc76c51a30654be30, + 0xd192e819d6ef5218, + 0xd69906245565a910, + 0xf40e35855771202a, + 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, + 0x1e376c085141ab53, + 0x2748774cdf8eeb99, + 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, + 0x4ed8aa4ae3418acb, + 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, + 0x78a5636f43172f60, + 0x84c87814a1f0ab72, + 0x8cc702081a6439ec, + 0x90befffa23631e28, + 0xa4506cebde82bde9, + 0xbef9a3f7b2c67915, + 0xc67178f2e372532b, + 0xca273eceea26619c, + 0xd186b8c721c0c207, + 0xeada7dd6cde0eb1e, + 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, + 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, + 0x1b710b35131c471b, + 0x28db77f523047d84, + 0x32caab7b40c72493, + 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, + 0x597f299cfc657e2a, + 0x5fcb6fab3ad6faec, + 0x6c44198c4a475817, +} + +func sha384Block(dh *[8]uint64, p *[128]byte) { + var w [80]uint64 + for i := range 80 { + if i < 16 { + w[i] = beUint64(p[i*8:]) + } else { + v1 := w[i-2] + t1 := bits.RotateLeft64(v1, -19) ^ bits.RotateLeft64(v1, -61) ^ (v1 >> 6) + v2 := w[i-15] + t2 := bits.RotateLeft64(v2, -1) ^ bits.RotateLeft64(v2, -8) ^ (v2 >> 7) + + w[i] = t1 + w[i-7] + t2 + w[i-16] + } + } + + a, b, c, d, e, f, g, h := dh[0], dh[1], dh[2], dh[3], dh[4], dh[5], dh[6], dh[7] + + for i := range 80 { + t1 := h + (bits.RotateLeft64(e, -14) ^ bits.RotateLeft64(e, -18) ^ + bits.RotateLeft64(e, -41)) + ((e & f) ^ (^e & g)) + _K[i] + w[i] + t2 := (bits.RotateLeft64(a, -28) ^ bits.RotateLeft64(a, -34) ^ + bits.RotateLeft64(a, -39)) + ((a & b) ^ (a & c) ^ (b & c)) + + h = g + g = f + f = e + e = d + t1 + d = c + c = b + b = a + a = t1 + t2 + } + + dh[0] += a + dh[1] += b + dh[2] += c + dh[3] += d + dh[4] += e + dh[5] += f + dh[6] += g + dh[7] += h +} + +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} diff --git a/src/crypto/internal/fips140/fips140.go b/src/crypto/internal/fips140/fips140.go index d03219b540e27f..76054b00684e2b 100644 --- a/src/crypto/internal/fips140/fips140.go +++ b/src/crypto/internal/fips140/fips140.go @@ -48,6 +48,8 @@ func Supported() error { } // See EnableFIPS in cmd/internal/obj/fips.go for commentary. + // Also, js/wasm and windows/386 don't have good enough timers + // for the CPU jitter entropy source. switch { case runtime.GOARCH == "wasm", runtime.GOOS == "windows" && runtime.GOARCH == "386", diff --git a/src/crypto/internal/fips140deps/fipsdeps_test.go b/src/crypto/internal/fips140deps/fipsdeps_test.go index 2c3bc8184e71bc..97552dc1ce10f1 100644 --- a/src/crypto/internal/fips140deps/fipsdeps_test.go +++ b/src/crypto/internal/fips140deps/fipsdeps_test.go @@ -88,7 +88,8 @@ func TestImports(t *testing.T) { } } - // Ensure that all packages except check and check's dependencies import check. + // Ensure that all packages except check, check's dependencies, and the + // entropy source (which is used only from .../fips140/drbg) import check. for pkg := range allPackages { switch pkg { case "crypto/internal/fips140/check": @@ -99,6 +100,7 @@ func TestImports(t *testing.T) { case "crypto/internal/fips140/sha3": case "crypto/internal/fips140/sha256": case "crypto/internal/fips140/sha512": + case "crypto/internal/fips140/entropy": default: if !importCheck[pkg] { t.Errorf("package %s does not import crypto/internal/fips140/check", pkg) diff --git a/src/crypto/internal/fips140deps/time/time.go b/src/crypto/internal/fips140deps/time/time.go new file mode 100644 index 00000000000000..eea37b772e4351 --- /dev/null +++ b/src/crypto/internal/fips140deps/time/time.go @@ -0,0 +1,21 @@ +// 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. + +//go:build !windows + +package time + +import "time" + +var start = time.Now() + +// HighPrecisionNow returns a high-resolution timestamp suitable for measuring +// small time differences. It uses the time package's monotonic clock. +// +// Its unit, epoch, and resolution are unspecified, and may change, but can be +// assumed to be sufficiently precise to measure time differences on the order +// of tens to hundreds of nanoseconds. +func HighPrecisionNow() int64 { + return int64(time.Since(start)) +} diff --git a/src/crypto/internal/fips140deps/time/time_windows.go b/src/crypto/internal/fips140deps/time/time_windows.go new file mode 100644 index 00000000000000..410ede4ee91705 --- /dev/null +++ b/src/crypto/internal/fips140deps/time/time_windows.go @@ -0,0 +1,17 @@ +// 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 time + +import "internal/syscall/windows" + +// HighPrecisionNow returns a high-resolution timestamp suitable for measuring +// small time differences. It uses Windows' QueryPerformanceCounter. +// +// Its unit, epoch, and resolution are unspecified, and may change, but can be +// assumed to be sufficiently precise to measure time differences on the order +// of tens to hundreds of nanoseconds. +func HighPrecisionNow() int64 { + return windows.QueryPerformanceCounter() +} diff --git a/src/crypto/internal/fips140test/entropy_test.go b/src/crypto/internal/fips140test/entropy_test.go new file mode 100644 index 00000000000000..76c24289520e17 --- /dev/null +++ b/src/crypto/internal/fips140test/entropy_test.go @@ -0,0 +1,264 @@ +// 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. + +//go:build !fips140v1.0 + +package fipstest + +import ( + "bytes" + "crypto/internal/cryptotest" + "crypto/internal/fips140/drbg" + "crypto/internal/fips140/entropy" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "flag" + "fmt" + "internal/testenv" + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" +) + +var flagEntropySamples = flag.String("entropy-samples", "", "store entropy samples with the provided `suffix`") +var flagNISTSP80090B = flag.Bool("nist-sp800-90b", false, "run NIST SP 800-90B tests (requires docker)") + +func TestEntropySamples(t *testing.T) { + cryptotest.MustSupportFIPS140(t) + + var seqSamples [1_000_000]uint8 + samplesOrTryAgain(t, seqSamples[:]) + seqSamplesName := fmt.Sprintf("entropy_samples_sequential_%s_%s_%s_%s_%s.bin", entropy.Version(), + runtime.GOOS, runtime.GOARCH, *flagEntropySamples, time.Now().Format("20060102T150405Z")) + if *flagEntropySamples != "" { + if err := os.WriteFile(seqSamplesName, seqSamples[:], 0644); err != nil { + t.Fatalf("failed to write samples to %q: %v", seqSamplesName, err) + } + t.Logf("wrote %s", seqSamplesName) + } + + var restartSamples [1000][1000]uint8 + for i := range restartSamples { + var samples [1024]uint8 + samplesOrTryAgain(t, samples[:]) + copy(restartSamples[i][:], samples[:]) + } + restartSamplesName := fmt.Sprintf("entropy_samples_restart_%s_%s_%s_%s_%s.bin", entropy.Version(), + runtime.GOOS, runtime.GOARCH, *flagEntropySamples, time.Now().Format("20060102T150405Z")) + if *flagEntropySamples != "" { + f, err := os.Create(restartSamplesName) + if err != nil { + t.Fatalf("failed to create %q: %v", restartSamplesName, err) + } + for i := range restartSamples { + if _, err := f.Write(restartSamples[i][:]); err != nil { + t.Fatalf("failed to write samples to %q: %v", restartSamplesName, err) + } + } + if err := f.Close(); err != nil { + t.Fatalf("failed to close %q: %v", restartSamplesName, err) + } + t.Logf("wrote %s", restartSamplesName) + } + + if *flagNISTSP80090B { + if *flagEntropySamples == "" { + t.Fatalf("-nist-sp800-90b requires -entropy-samples to be set too") + } + + // Check if the nist-sp800-90b docker image is already present, + // and build it otherwise. + if err := testenv.Command(t, + "docker", "image", "inspect", "nist-sp800-90b", + ).Run(); err != nil { + t.Logf("building nist-sp800-90b docker image") + dockerfile := filepath.Join(t.TempDir(), "Dockerfile.SP800-90B_EntropyAssessment") + if err := os.WriteFile(dockerfile, []byte(NISTSP80090BDockerfile), 0644); err != nil { + t.Fatalf("failed to write Dockerfile: %v", err) + } + out, err := testenv.Command(t, + "docker", "build", "-t", "nist-sp800-90b", "-f", dockerfile, "/var/empty", + ).CombinedOutput() + if err != nil { + t.Fatalf("failed to build nist-sp800-90b docker image: %v\n%s", err, out) + } + } + + pwd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get current working directory: %v", err) + } + t.Logf("running ea_non_iid analysis") + out, err := testenv.Command(t, + "docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd, + "nist-sp800-90b", "ea_non_iid", seqSamplesName, "8", + ).CombinedOutput() + if err != nil { + t.Fatalf("ea_non_iid failed: %v\n%s", err, out) + } + t.Logf("\n%s", out) + + H_I := string(out) + H_I = strings.TrimSpace(H_I[strings.LastIndexByte(H_I, ' ')+1:]) + t.Logf("running ea_restart analysis with H_I = %s", H_I) + out, err = testenv.Command(t, + "docker", "run", "--rm", "-v", fmt.Sprintf("%s:%s", pwd, pwd), "-w", pwd, + "nist-sp800-90b", "ea_restart", restartSamplesName, "8", H_I, + ).CombinedOutput() + if err != nil { + t.Fatalf("ea_restart failed: %v\n%s", err, out) + } + t.Logf("\n%s", out) + } +} + +var NISTSP80090BDockerfile = ` +FROM ubuntu:24.04 +RUN apt-get update && apt-get install -y build-essential git \ + libbz2-dev libdivsufsort-dev libjsoncpp-dev libgmp-dev libmpfr-dev libssl-dev \ + && rm -rf /var/lib/apt/lists/* +RUN git clone --depth 1 https://github.com/usnistgov/SP800-90B_EntropyAssessment.git +RUN cd SP800-90B_EntropyAssessment && git checkout 8924f158c97e7b805e0f95247403ad4c44b9cd6f +WORKDIR ./SP800-90B_EntropyAssessment/cpp/ +RUN make all +RUN cd selftest && ./selftest +RUN cp ea_non_iid ea_restart /usr/local/bin/ +` + +var memory entropy.ScratchBuffer + +// samplesOrTryAgain calls entropy.Samples up to 10 times until it succeeds. +// Samples has a non-negligible chance of failing the health tests, as required +// by SP 800-90B. +func samplesOrTryAgain(t *testing.T, samples []uint8) { + t.Helper() + for range 10 { + if err := entropy.Samples(samples, &memory); err != nil { + t.Logf("entropy.Samples() failed: %v", err) + continue + } + return + } + t.Fatal("entropy.Samples() failed 10 times in a row") +} + +func TestEntropySHA384(t *testing.T) { + var input [1024]uint8 + for i := range input { + input[i] = uint8(i) + } + want := sha512.Sum384(input[:]) + got := entropy.SHA384(&input) + if got != want { + t.Errorf("SHA384() = %x, want %x", got, want) + } +} + +func TestEntropyRepetitionCountTest(t *testing.T) { + good := bytes.Repeat(append(bytes.Repeat([]uint8{42}, 40), 1), 100) + if err := entropy.RepetitionCountTest(good); err != nil { + t.Errorf("RepetitionCountTest(good) = %v, want nil", err) + } + + bad := bytes.Repeat([]uint8{0}, 40) + bad = append(bad, bytes.Repeat([]uint8{1}, 40)...) + bad = append(bad, bytes.Repeat([]uint8{42}, 41)...) + bad = append(bad, bytes.Repeat([]uint8{2}, 40)...) + if err := entropy.RepetitionCountTest(bad); err == nil { + t.Error("RepetitionCountTest(bad) = nil, want error") + } + + bad = bytes.Repeat([]uint8{42}, 41) + if err := entropy.RepetitionCountTest(bad); err == nil { + t.Error("RepetitionCountTest(bad) = nil, want error") + } +} + +func TestEntropyAdaptiveProportionTest(t *testing.T) { + good := bytes.Repeat([]uint8{0}, 409) + good = append(good, bytes.Repeat([]uint8{1}, 512-409)...) + good = append(good, bytes.Repeat([]uint8{0}, 409)...) + if err := entropy.AdaptiveProportionTest(good); err != nil { + t.Errorf("AdaptiveProportionTest(good) = %v, want nil", err) + } + + // These fall out of the window. + bad := bytes.Repeat([]uint8{1}, 100) + bad = append(bad, bytes.Repeat([]uint8{1, 2, 3, 4, 5, 6}, 100)...) + // These are in the window. + bad = append(bad, bytes.Repeat([]uint8{42}, 410)...) + if err := entropy.AdaptiveProportionTest(bad[:len(bad)-1]); err != nil { + t.Errorf("AdaptiveProportionTest(bad[:len(bad)-1]) = %v, want nil", err) + } + if err := entropy.AdaptiveProportionTest(bad); err == nil { + t.Error("AdaptiveProportionTest(bad) = nil, want error") + } +} + +func TestEntropyUnchanged(t *testing.T) { + testenv.MustHaveSource(t) + + h := sha256.New() + root := os.DirFS("../fips140/entropy") + if err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + data, err := fs.ReadFile(root, path) + if err != nil { + return err + } + t.Logf("Hashing %s (%d bytes)", path, len(data)) + fmt.Fprintf(h, "%s %d\n", path, len(data)) + h.Write(data) + return nil + }); err != nil { + t.Fatalf("WalkDir: %v", err) + } + + // The crypto/internal/fips140/entropy package is certified as a FIPS 140-3 + // entropy source through the Entropy Source Validation program, + // independently of the FIPS 140-3 module. It must not change even across + // FIPS 140-3 module versions, in order to reuse the ESV certificate. + exp := "35976eb8a11678c79777da07aaab5511d4325701f837777df205f6e7b20c6821" + if got := hex.EncodeToString(h.Sum(nil)); got != exp { + t.Errorf("hash of crypto/internal/fips140/entropy = %s, want %s", got, exp) + } +} + +func TestEntropyRace(t *testing.T) { + // Check that concurrent calls to Seed don't trigger the race detector. + for range 2 { + go func() { + _, _ = entropy.Seed(&memory) + }() + } + // Same, with the higher-level DRBG. More concurrent calls to hit the Pool. + for range 16 { + go func() { + var b [64]byte + drbg.Read(b[:]) + }() + } +} + +var sink byte + +func BenchmarkEntropySeed(b *testing.B) { + for b.Loop() { + seed, err := entropy.Seed(&memory) + if err != nil { + b.Fatalf("entropy.Seed() failed: %v", err) + } + sink ^= seed[0] + } +} diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index d50a98d34c9092..c76b254b23ffc8 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -485,11 +485,16 @@ var depsRules = ` internal/byteorder < crypto/internal/fips140deps/byteorder; internal/cpu, internal/goarch < crypto/internal/fips140deps/cpu; internal/godebug < crypto/internal/fips140deps/godebug; + time, internal/syscall/windows < crypto/internal/fips140deps/time; + + crypto/internal/fips140deps/time, errors, math/bits, sync/atomic, unsafe + < crypto/internal/fips140/entropy; STR, hash, crypto/internal/impl, crypto/internal/entropy, crypto/internal/randutil, + crypto/internal/fips140/entropy, crypto/internal/fips140deps/byteorder, crypto/internal/fips140deps/cpu, crypto/internal/fips140deps/godebug From 75c87df58ebfb24592d7002ef71912f8708dc424 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 16 Sep 2025 10:52:49 +0200 Subject: [PATCH 14/63] internal/poll: pass the I/O mode instead of an overlapped object in execIO execIO callers should be agnostic to the fact that it uses an overlapped object. This will unlock future optimizations and simplifications. Change-Id: I0a58d992101fa74ac75e3538af04cbc44156f0d6 Reviewed-on: https://go-review.googlesource.com/c/go/+/704175 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao --- src/internal/poll/fd_windows.go | 64 ++++++++++++++------------- src/internal/poll/sendfile_windows.go | 8 +--- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go index 9323e49eb745d7..a6ecdafc3404c5 100644 --- a/src/internal/poll/fd_windows.go +++ b/src/internal/poll/fd_windows.go @@ -250,12 +250,16 @@ func (fd *FD) cancelIO(o *operation) { // It supports both synchronous and asynchronous IO. // o.qty and o.flags are set to zero before calling submit // to avoid reusing the values from a previous call. -func (fd *FD) execIO(o *operation, submit func(o *operation) (uint32, error)) (int, error) { +func (fd *FD) execIO(mode int, submit func(o *operation) (uint32, error)) (int, error) { // Notify runtime netpoll about starting IO. - err := fd.pd.prepare(int(o.mode), fd.isFile) + err := fd.pd.prepare(mode, fd.isFile) if err != nil { return 0, err } + o := &fd.rop + if mode == 'w' { + o = &fd.wop + } // Start IO. if !fd.isBlocking && o.o.HEvent == 0 && !fd.pollable() { // If the handle is opened for overlapped IO but we can't @@ -552,7 +556,7 @@ func (fd *FD) Read(buf []byte) (int, error) { case kindConsole: n, err = fd.readConsole(buf) case kindFile, kindPipe: - n, err = fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err = fd.execIO('r', func(o *operation) (qty uint32, err error) { err = syscall.ReadFile(fd.Sysfd, buf, &qty, fd.overlapped(o)) return qty, err }) @@ -567,7 +571,7 @@ func (fd *FD) Read(buf []byte) (int, error) { } } case kindNet: - n, err = fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err = fd.execIO('r', func(o *operation) (qty uint32, err error) { var flags uint32 err = syscall.WSARecv(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, &o.o, nil) return qty, err @@ -694,7 +698,7 @@ func (fd *FD) Pread(buf []byte, off int64) (int, error) { defer fd.setOffset(curoffset) } fd.setOffset(off) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { err = syscall.ReadFile(fd.Sysfd, buf, &qty, &o.o) return qty, err }) @@ -727,7 +731,7 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) { rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { rsan := int32(unsafe.Sizeof(*rsa)) var flags uint32 err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil) @@ -761,7 +765,7 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { rsan := int32(unsafe.Sizeof(*rsa)) var flags uint32 err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil) @@ -795,7 +799,7 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { rsan := int32(unsafe.Sizeof(*rsa)) var flags uint32 err = syscall.WSARecvFrom(fd.Sysfd, newWsaBuf(buf), 1, &qty, &flags, rsa, &rsan, &o.o, nil) @@ -841,7 +845,7 @@ func (fd *FD) Write(buf []byte) (int, error) { case kindConsole: n, err = fd.writeConsole(b) case kindPipe, kindFile: - n, err = fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err = fd.execIO('w', func(o *operation) (qty uint32, err error) { err = syscall.WriteFile(fd.Sysfd, b, &qty, fd.overlapped(o)) return qty, err }) @@ -850,7 +854,7 @@ func (fd *FD) Write(buf []byte) (int, error) { if race.Enabled { race.ReleaseMerge(unsafe.Pointer(&ioSync)) } - n, err = fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err = fd.execIO('w', func(o *operation) (qty uint32, err error) { err = syscall.WSASend(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, &o.o, nil) return qty, err }) @@ -949,7 +953,7 @@ func (fd *FD) Pwrite(buf []byte, off int64) (int, error) { max = ntotal + maxRW } fd.setOffset(off + int64(ntotal)) - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = syscall.WriteFile(fd.Sysfd, buf[ntotal:max], &qty, &o.o) return qty, err }) @@ -979,7 +983,7 @@ func (fd *FD) Writev(buf *[][]byte) (int64, error) { } bufs := newWSABufs(buf) defer freeWSABufs(bufs) - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = syscall.WSASend(fd.Sysfd, &(*bufs)[0], uint32(len(*bufs)), &qty, 0, &o.o, nil) return qty, err }) @@ -997,7 +1001,7 @@ func (fd *FD) WriteTo(buf []byte, sa syscall.Sockaddr) (int, error) { if len(buf) == 0 { // handle zero-byte payload - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = syscall.WSASendto(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, 0, sa, &o.o, nil) return qty, err }) @@ -1015,7 +1019,7 @@ func (fd *FD) WriteTo(buf []byte, sa syscall.Sockaddr) (int, error) { if len(b) > maxRW { b = b[:maxRW] } - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = syscall.WSASendto(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, sa, &o.o, nil) return qty, err }) @@ -1037,7 +1041,7 @@ func (fd *FD) WriteToInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) if len(buf) == 0 { // handle zero-byte payload - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendtoInet4(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, 0, sa4, &o.o, nil) return qty, err }) @@ -1055,7 +1059,7 @@ func (fd *FD) WriteToInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) if len(b) > maxRW { b = b[:maxRW] } - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendtoInet4(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, sa4, &o.o, nil) return qty, err }) @@ -1077,7 +1081,7 @@ func (fd *FD) WriteToInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) if len(buf) == 0 { // handle zero-byte payload - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendtoInet6(fd.Sysfd, &syscall.WSABuf{}, 1, &qty, 0, sa6, &o.o, nil) return qty, err }) @@ -1095,7 +1099,7 @@ func (fd *FD) WriteToInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) if len(b) > maxRW { b = b[:maxRW] } - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendtoInet6(fd.Sysfd, newWsaBuf(b), 1, &qty, 0, sa6, &o.o, nil) return qty, err }) @@ -1112,17 +1116,16 @@ func (fd *FD) WriteToInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) // called when the descriptor is first created. This is here rather // than in the net package so that it can use fd.wop. func (fd *FD) ConnectEx(ra syscall.Sockaddr) error { - o := &fd.wop - _, err := fd.execIO(o, func(o *operation) (uint32, error) { + _, err := fd.execIO('w', func(o *operation) (uint32, error) { return 0, ConnectExFunc(fd.Sysfd, ra, nil, 0, nil, &o.o) }) return err } -func (fd *FD) acceptOne(s syscall.Handle, rawsa []syscall.RawSockaddrAny, o *operation) (string, error) { +func (fd *FD) acceptOne(s syscall.Handle, rawsa []syscall.RawSockaddrAny) (string, error) { // Submit accept request. rsan := uint32(unsafe.Sizeof(rawsa[0])) - _, err := fd.execIO(o, func(o *operation) (qty uint32, err error) { + _, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { err = AcceptFunc(fd.Sysfd, s, (*byte)(unsafe.Pointer(&rawsa[0])), 0, rsan, rsan, &qty, &o.o) return qty, err @@ -1150,7 +1153,6 @@ func (fd *FD) Accept(sysSocket func() (syscall.Handle, error)) (syscall.Handle, } defer fd.readUnlock() - o := &fd.rop var rawsa [2]syscall.RawSockaddrAny for { s, err := sysSocket() @@ -1158,7 +1160,7 @@ func (fd *FD) Accept(sysSocket func() (syscall.Handle, error)) (syscall.Handle, return syscall.InvalidHandle, nil, 0, "", err } - errcall, err := fd.acceptOne(s, rawsa[:], o) + errcall, err := fd.acceptOne(s, rawsa[:]) if err == nil { return s, rawsa[:], uint32(unsafe.Sizeof(rawsa[0])), "", nil } @@ -1286,7 +1288,7 @@ func (fd *FD) RawRead(f func(uintptr) bool) error { // Use a zero-byte read as a way to get notified when this // socket is readable. h/t https://stackoverflow.com/a/42019668/332798 - _, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + _, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { var flags uint32 if !fd.IsStream { flags |= windows.MSG_PEEK @@ -1381,7 +1383,7 @@ func (fd *FD) ReadMsg(p []byte, oob []byte, flags int) (int, int, int, syscall.S msg := newWSAMsg(p, oob, flags, true) defer freeWSAMsg(msg) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil) return qty, err }) @@ -1406,7 +1408,7 @@ func (fd *FD) ReadMsgInet4(p []byte, oob []byte, flags int, sa4 *syscall.Sockadd msg := newWSAMsg(p, oob, flags, true) defer freeWSAMsg(msg) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil) return qty, err }) @@ -1430,7 +1432,7 @@ func (fd *FD) ReadMsgInet6(p []byte, oob []byte, flags int, sa6 *syscall.Sockadd msg := newWSAMsg(p, oob, flags, true) defer freeWSAMsg(msg) - n, err := fd.execIO(&fd.rop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('r', func(o *operation) (qty uint32, err error) { err = windows.WSARecvMsg(fd.Sysfd, msg, &qty, &o.o, nil) return qty, err }) @@ -1461,7 +1463,7 @@ func (fd *FD) WriteMsg(p []byte, oob []byte, sa syscall.Sockaddr) (int, int, err return 0, 0, err } } - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil) return qty, err }) @@ -1484,7 +1486,7 @@ func (fd *FD) WriteMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (in if sa != nil { msg.Namelen = sockaddrInet4ToRaw(msg.Name, sa) } - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil) return qty, err }) @@ -1507,7 +1509,7 @@ func (fd *FD) WriteMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (in if sa != nil { msg.Namelen = sockaddrInet6ToRaw(msg.Name, sa) } - n, err := fd.execIO(&fd.wop, func(o *operation) (qty uint32, err error) { + n, err := fd.execIO('w', func(o *operation) (qty uint32, err error) { err = windows.WSASendMsg(fd.Sysfd, msg, 0, nil, &o.o, nil) return qty, err }) diff --git a/src/internal/poll/sendfile_windows.go b/src/internal/poll/sendfile_windows.go index a052f4a1f82400..2bdfecf0134b62 100644 --- a/src/internal/poll/sendfile_windows.go +++ b/src/internal/poll/sendfile_windows.go @@ -62,18 +62,14 @@ func SendFile(fd *FD, src uintptr, size int64) (written int64, err error, handle // See https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-transmitfile const maxChunkSizePerCall = int64(0x7fffffff - 1) - o := &fd.wop for size > 0 { chunkSize := maxChunkSizePerCall if chunkSize > size { chunkSize = size } - off := startpos + written - o.o.Offset = uint32(off) - o.o.OffsetHigh = uint32(off >> 32) - - n, err := fd.execIO(o, func(o *operation) (uint32, error) { + fd.setOffset(startpos + written) + n, err := fd.execIO('w', func(o *operation) (uint32, error) { err := syscall.TransmitFile(fd.Sysfd, hsrc, uint32(chunkSize), 0, &o.o, nil, syscall.TF_WRITE_BEHIND) if err != nil { return 0, err From db10db6be361f4eaf5f81890e487a9cbf3ca5a53 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Tue, 16 Sep 2025 14:12:25 +0200 Subject: [PATCH 15/63] internal/poll: remove operation fields from FD Use a sync.Pool to reuse the overlapped object passed to the different Windows syscalls instead of keeping two of them in the FD struct. This reduces the size of the FD struct from 248 to 152 bytes. While here, pin the overlapped object for the duration of the overlapped IO operation to comply with the memory safety rules. Cq-Include-Trybots: luci.golang.try:gotip-windows-amd64-longtest,gotip-windows-amd64-race Change-Id: I0161d163f681fe94b822c0c885aaa42c449e5342 Reviewed-on: https://go-review.googlesource.com/c/go/+/704235 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao --- src/internal/poll/fd_windows.go | 132 ++++++++++++++------------------ 1 file changed, 59 insertions(+), 73 deletions(-) diff --git a/src/internal/poll/fd_windows.go b/src/internal/poll/fd_windows.go index a6ecdafc3404c5..6443f6eb30b8a5 100644 --- a/src/internal/poll/fd_windows.go +++ b/src/internal/poll/fd_windows.go @@ -78,22 +78,6 @@ type operation struct { mode int32 } -func (o *operation) setEvent() { - h, err := windows.CreateEvent(nil, 0, 0, nil) - if err != nil { - // This shouldn't happen when all CreateEvent arguments are zero. - panic(err) - } - // Set the low bit so that the external IOCP doesn't receive the completion packet. - o.o.HEvent = h | 1 -} - -func (o *operation) close() { - if o.o.HEvent != 0 { - syscall.CloseHandle(o.o.HEvent) - } -} - func (fd *FD) overlapped(o *operation) *syscall.Overlapped { if fd.isBlocking { // Don't return the overlapped object if the file handle @@ -208,6 +192,12 @@ var wsaRsaPool = sync.Pool{ }, } +var operationPool = sync.Pool{ + New: func() any { + return new(operation) + }, +} + // waitIO waits for the IO operation o to complete. func (fd *FD) waitIO(o *operation) error { if fd.isBlocking { @@ -246,27 +236,57 @@ func (fd *FD) cancelIO(o *operation) { fd.pd.waitCanceled(int(o.mode)) } +// pin pins ptr for the duration of the IO operation. +// If fd is in blocking mode, pin does nothing. +func (fd *FD) pin(mode int, ptr any) { + if fd.isBlocking { + return + } + if mode == 'r' { + fd.readPinner.Pin(ptr) + } else { + fd.writePinner.Pin(ptr) + } +} + // execIO executes a single IO operation o. // It supports both synchronous and asynchronous IO. -// o.qty and o.flags are set to zero before calling submit -// to avoid reusing the values from a previous call. func (fd *FD) execIO(mode int, submit func(o *operation) (uint32, error)) (int, error) { + if mode == 'r' { + defer fd.readPinner.Unpin() + } else { + defer fd.writePinner.Unpin() + } // Notify runtime netpoll about starting IO. err := fd.pd.prepare(mode, fd.isFile) if err != nil { return 0, err } - o := &fd.rop - if mode == 'w' { - o = &fd.wop + o := operationPool.Get().(*operation) + defer operationPool.Put(o) + *o = operation{ + o: syscall.Overlapped{ + OffsetHigh: uint32(fd.offset >> 32), + Offset: uint32(fd.offset), + }, + runtimeCtx: fd.pd.runtimeCtx, + mode: int32(mode), } // Start IO. - if !fd.isBlocking && o.o.HEvent == 0 && !fd.pollable() { + if !fd.isBlocking && !fd.pollable() { // If the handle is opened for overlapped IO but we can't // use the runtime poller, then we need to use an // event to wait for the IO to complete. - o.setEvent() + h, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + // This shouldn't happen when all CreateEvent arguments are zero. + panic(err) + } + // Set the low bit so that the external IOCP doesn't receive the completion packet. + o.o.HEvent = h | 1 + defer syscall.CloseHandle(h) } + fd.pin(mode, o) qty, err := submit(o) var waitErr error // Blocking operations shouldn't return ERROR_IO_PENDING. @@ -321,11 +341,6 @@ type FD struct { // System file descriptor. Immutable until Close. Sysfd syscall.Handle - // Read operation. - rop operation - // Write operation. - wop operation - // I/O poller. pd pollDesc @@ -364,6 +379,8 @@ type FD struct { disassociated atomic.Bool + // readPinner and writePinner are automatically unpinned + // before execIO returns. readPinner runtime.Pinner writePinner runtime.Pinner } @@ -383,8 +400,6 @@ type FD struct { // using an external mechanism. func (fd *FD) setOffset(off int64) { fd.offset = off - fd.rop.o.OffsetHigh, fd.rop.o.Offset = uint32(off>>32), uint32(off) - fd.wop.o.OffsetHigh, fd.wop.o.Offset = uint32(off>>32), uint32(off) } // addOffset adds the given offset to the current offset. @@ -435,8 +450,6 @@ func (fd *FD) Init(net string, pollable bool) error { } fd.isFile = fd.kind != kindNet fd.isBlocking = !pollable - fd.rop.mode = 'r' - fd.wop.mode = 'w' // It is safe to add overlapped handles that also perform I/O // outside of the runtime poller. The runtime poller will ignore @@ -445,8 +458,6 @@ func (fd *FD) Init(net string, pollable bool) error { if err != nil { return err } - fd.rop.runtimeCtx = fd.pd.runtimeCtx - fd.wop.runtimeCtx = fd.pd.runtimeCtx if fd.kind != kindNet || socketCanUseSetFileCompletionNotificationModes { // Non-socket handles can use SetFileCompletionNotificationModes without problems. err := syscall.SetFileCompletionNotificationModes(fd.Sysfd, @@ -485,8 +496,6 @@ func (fd *FD) destroy() error { if fd.Sysfd == syscall.InvalidHandle { return syscall.EINVAL } - fd.rop.close() - fd.wop.close() // Poller may want to unregister fd in readiness notification mechanism, // so this must be executed before fd.CloseFunc. fd.pd.close() @@ -541,9 +550,8 @@ func (fd *FD) Read(buf []byte) (int, error) { defer fd.readUnlock() } - if len(buf) > 0 && !fd.isBlocking { - fd.readPinner.Pin(&buf[0]) - defer fd.readPinner.Unpin() + if len(buf) > 0 { + fd.pin('r', &buf[0]) } if len(buf) > maxRW { @@ -672,9 +680,8 @@ func (fd *FD) Pread(buf []byte, off int64) (int, error) { } defer fd.readWriteUnlock() - if len(buf) > 0 && !fd.isBlocking { - fd.readPinner.Pin(&buf[0]) - defer fd.readPinner.Unpin() + if len(buf) > 0 { + fd.pin('r', &buf[0]) } if len(buf) > maxRW { @@ -724,10 +731,7 @@ func (fd *FD) ReadFrom(buf []byte) (int, syscall.Sockaddr, error) { } defer fd.readUnlock() - if !fd.isBlocking { - fd.readPinner.Pin(&buf[0]) - defer fd.readPinner.Unpin() - } + fd.pin('r', &buf[0]) rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) @@ -758,10 +762,7 @@ func (fd *FD) ReadFromInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) } defer fd.readUnlock() - if !fd.isBlocking { - fd.readPinner.Pin(&buf[0]) - defer fd.readPinner.Unpin() - } + fd.pin('r', &buf[0]) rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) @@ -792,10 +793,7 @@ func (fd *FD) ReadFromInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) } defer fd.readUnlock() - if !fd.isBlocking { - fd.readPinner.Pin(&buf[0]) - defer fd.readPinner.Unpin() - } + fd.pin('r', &buf[0]) rsa := wsaRsaPool.Get().(*syscall.RawSockaddrAny) defer wsaRsaPool.Put(rsa) @@ -827,11 +825,9 @@ func (fd *FD) Write(buf []byte) (int, error) { defer fd.writeUnlock() } - if len(buf) > 0 && !fd.isBlocking { - fd.writePinner.Pin(&buf[0]) - defer fd.writePinner.Unpin() + if len(buf) > 0 { + fd.pin('w', &buf[0]) } - var ntotal int for { max := len(buf) @@ -924,9 +920,8 @@ func (fd *FD) Pwrite(buf []byte, off int64) (int, error) { } defer fd.readWriteUnlock() - if len(buf) > 0 && !fd.isBlocking { - fd.writePinner.Pin(&buf[0]) - defer fd.writePinner.Unpin() + if len(buf) > 0 { + fd.pin('w', &buf[0]) } if fd.isBlocking { @@ -1008,10 +1003,7 @@ func (fd *FD) WriteTo(buf []byte, sa syscall.Sockaddr) (int, error) { return n, err } - if !fd.isBlocking { - fd.writePinner.Pin(&buf[0]) - defer fd.writePinner.Unpin() - } + fd.pin('w', &buf[0]) ntotal := 0 for len(buf) > 0 { @@ -1048,10 +1040,7 @@ func (fd *FD) WriteToInet4(buf []byte, sa4 *syscall.SockaddrInet4) (int, error) return n, err } - if !fd.isBlocking { - fd.writePinner.Pin(&buf[0]) - defer fd.writePinner.Unpin() - } + fd.pin('w', &buf[0]) ntotal := 0 for len(buf) > 0 { @@ -1088,10 +1077,7 @@ func (fd *FD) WriteToInet6(buf []byte, sa6 *syscall.SockaddrInet6) (int, error) return n, err } - if !fd.isBlocking { - fd.writePinner.Pin(&buf[0]) - defer fd.writePinner.Unpin() - } + fd.pin('w', &buf[0]) ntotal := 0 for len(buf) > 0 { From 742f92063e5acc3671a0bd9982e7678d864008f0 Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Mon, 29 Sep 2025 16:14:24 -0400 Subject: [PATCH 16/63] cmd/compile, runtime: always enable Wasm signext and satconv features These features have been standardized since at least Wasm 2.0. Always enable them. The corresponding GOWASM settings are now no-op. Change-Id: I0e59f21696a69a4e289127988aad629a720b002b Reviewed-on: https://go-review.googlesource.com/c/go/+/707855 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase --- src/cmd/compile/internal/ssa/_gen/Wasm.rules | 9 +- src/cmd/compile/internal/ssa/rewriteWasm.go | 109 ------------------- src/cmd/compile/internal/wasm/ssa.go | 21 +--- src/internal/buildcfg/cfg.go | 25 ++--- src/runtime/conv_wasm_test.go | 45 ++++---- src/runtime/sys_wasm.s | 58 ---------- 6 files changed, 38 insertions(+), 229 deletions(-) diff --git a/src/cmd/compile/internal/ssa/_gen/Wasm.rules b/src/cmd/compile/internal/ssa/_gen/Wasm.rules index f3bd8d8b4f18f1..f632a01109f764 100644 --- a/src/cmd/compile/internal/ssa/_gen/Wasm.rules +++ b/src/cmd/compile/internal/ssa/_gen/Wasm.rules @@ -55,12 +55,9 @@ (ZeroExt32to64 x:(I64Load32U _ _)) => x (ZeroExt16to(64|32) x:(I64Load16U _ _)) => x (ZeroExt8to(64|32|16) x:(I64Load8U _ _)) => x -(SignExt32to64 x) && buildcfg.GOWASM.SignExt => (I64Extend32S x) -(SignExt8to(64|32|16) x) && buildcfg.GOWASM.SignExt => (I64Extend8S x) -(SignExt16to(64|32) x) && buildcfg.GOWASM.SignExt => (I64Extend16S x) -(SignExt32to64 x) => (I64ShrS (I64Shl x (I64Const [32])) (I64Const [32])) -(SignExt16to(64|32) x) => (I64ShrS (I64Shl x (I64Const [48])) (I64Const [48])) -(SignExt8to(64|32|16) x) => (I64ShrS (I64Shl x (I64Const [56])) (I64Const [56])) +(SignExt32to64 x) => (I64Extend32S x) +(SignExt8to(64|32|16) x) => (I64Extend8S x) +(SignExt16to(64|32) x) => (I64Extend16S x) (ZeroExt32to64 x) => (I64And x (I64Const [0xffffffff])) (ZeroExt16to(64|32) x) => (I64And x (I64Const [0xffff])) (ZeroExt8to(64|32|16) x) => (I64And x (I64Const [0xff])) diff --git a/src/cmd/compile/internal/ssa/rewriteWasm.go b/src/cmd/compile/internal/ssa/rewriteWasm.go index c3c5528aaa6ab4..a164a6eee555b9 100644 --- a/src/cmd/compile/internal/ssa/rewriteWasm.go +++ b/src/cmd/compile/internal/ssa/rewriteWasm.go @@ -2,7 +2,6 @@ package ssa -import "internal/buildcfg" import "math" import "cmd/compile/internal/types" @@ -3202,8 +3201,6 @@ func rewriteValueWasm_OpRsh8x8(v *Value) bool { } func rewriteValueWasm_OpSignExt16to32(v *Value) bool { v_0 := v.Args[0] - b := v.Block - typ := &b.Func.Config.Types // match: (SignExt16to32 x:(I64Load16S _ _)) // result: x for { @@ -3215,34 +3212,16 @@ func rewriteValueWasm_OpSignExt16to32(v *Value) bool { return true } // match: (SignExt16to32 x) - // cond: buildcfg.GOWASM.SignExt // result: (I64Extend16S x) for { x := v_0 - if !(buildcfg.GOWASM.SignExt) { - break - } v.reset(OpWasmI64Extend16S) v.AddArg(x) return true } - // match: (SignExt16to32 x) - // result: (I64ShrS (I64Shl x (I64Const [48])) (I64Const [48])) - for { - x := v_0 - v.reset(OpWasmI64ShrS) - v0 := b.NewValue0(v.Pos, OpWasmI64Shl, typ.Int64) - v1 := b.NewValue0(v.Pos, OpWasmI64Const, typ.Int64) - v1.AuxInt = int64ToAuxInt(48) - v0.AddArg2(x, v1) - v.AddArg2(v0, v1) - return true - } } func rewriteValueWasm_OpSignExt16to64(v *Value) bool { v_0 := v.Args[0] - b := v.Block - typ := &b.Func.Config.Types // match: (SignExt16to64 x:(I64Load16S _ _)) // result: x for { @@ -3254,34 +3233,16 @@ func rewriteValueWasm_OpSignExt16to64(v *Value) bool { return true } // match: (SignExt16to64 x) - // cond: buildcfg.GOWASM.SignExt // result: (I64Extend16S x) for { x := v_0 - if !(buildcfg.GOWASM.SignExt) { - break - } v.reset(OpWasmI64Extend16S) v.AddArg(x) return true } - // match: (SignExt16to64 x) - // result: (I64ShrS (I64Shl x (I64Const [48])) (I64Const [48])) - for { - x := v_0 - v.reset(OpWasmI64ShrS) - v0 := b.NewValue0(v.Pos, OpWasmI64Shl, typ.Int64) - v1 := b.NewValue0(v.Pos, OpWasmI64Const, typ.Int64) - v1.AuxInt = int64ToAuxInt(48) - v0.AddArg2(x, v1) - v.AddArg2(v0, v1) - return true - } } func rewriteValueWasm_OpSignExt32to64(v *Value) bool { v_0 := v.Args[0] - b := v.Block - typ := &b.Func.Config.Types // match: (SignExt32to64 x:(I64Load32S _ _)) // result: x for { @@ -3293,34 +3254,16 @@ func rewriteValueWasm_OpSignExt32to64(v *Value) bool { return true } // match: (SignExt32to64 x) - // cond: buildcfg.GOWASM.SignExt // result: (I64Extend32S x) for { x := v_0 - if !(buildcfg.GOWASM.SignExt) { - break - } v.reset(OpWasmI64Extend32S) v.AddArg(x) return true } - // match: (SignExt32to64 x) - // result: (I64ShrS (I64Shl x (I64Const [32])) (I64Const [32])) - for { - x := v_0 - v.reset(OpWasmI64ShrS) - v0 := b.NewValue0(v.Pos, OpWasmI64Shl, typ.Int64) - v1 := b.NewValue0(v.Pos, OpWasmI64Const, typ.Int64) - v1.AuxInt = int64ToAuxInt(32) - v0.AddArg2(x, v1) - v.AddArg2(v0, v1) - return true - } } func rewriteValueWasm_OpSignExt8to16(v *Value) bool { v_0 := v.Args[0] - b := v.Block - typ := &b.Func.Config.Types // match: (SignExt8to16 x:(I64Load8S _ _)) // result: x for { @@ -3332,34 +3275,16 @@ func rewriteValueWasm_OpSignExt8to16(v *Value) bool { return true } // match: (SignExt8to16 x) - // cond: buildcfg.GOWASM.SignExt // result: (I64Extend8S x) for { x := v_0 - if !(buildcfg.GOWASM.SignExt) { - break - } v.reset(OpWasmI64Extend8S) v.AddArg(x) return true } - // match: (SignExt8to16 x) - // result: (I64ShrS (I64Shl x (I64Const [56])) (I64Const [56])) - for { - x := v_0 - v.reset(OpWasmI64ShrS) - v0 := b.NewValue0(v.Pos, OpWasmI64Shl, typ.Int64) - v1 := b.NewValue0(v.Pos, OpWasmI64Const, typ.Int64) - v1.AuxInt = int64ToAuxInt(56) - v0.AddArg2(x, v1) - v.AddArg2(v0, v1) - return true - } } func rewriteValueWasm_OpSignExt8to32(v *Value) bool { v_0 := v.Args[0] - b := v.Block - typ := &b.Func.Config.Types // match: (SignExt8to32 x:(I64Load8S _ _)) // result: x for { @@ -3371,34 +3296,16 @@ func rewriteValueWasm_OpSignExt8to32(v *Value) bool { return true } // match: (SignExt8to32 x) - // cond: buildcfg.GOWASM.SignExt // result: (I64Extend8S x) for { x := v_0 - if !(buildcfg.GOWASM.SignExt) { - break - } v.reset(OpWasmI64Extend8S) v.AddArg(x) return true } - // match: (SignExt8to32 x) - // result: (I64ShrS (I64Shl x (I64Const [56])) (I64Const [56])) - for { - x := v_0 - v.reset(OpWasmI64ShrS) - v0 := b.NewValue0(v.Pos, OpWasmI64Shl, typ.Int64) - v1 := b.NewValue0(v.Pos, OpWasmI64Const, typ.Int64) - v1.AuxInt = int64ToAuxInt(56) - v0.AddArg2(x, v1) - v.AddArg2(v0, v1) - return true - } } func rewriteValueWasm_OpSignExt8to64(v *Value) bool { v_0 := v.Args[0] - b := v.Block - typ := &b.Func.Config.Types // match: (SignExt8to64 x:(I64Load8S _ _)) // result: x for { @@ -3410,29 +3317,13 @@ func rewriteValueWasm_OpSignExt8to64(v *Value) bool { return true } // match: (SignExt8to64 x) - // cond: buildcfg.GOWASM.SignExt // result: (I64Extend8S x) for { x := v_0 - if !(buildcfg.GOWASM.SignExt) { - break - } v.reset(OpWasmI64Extend8S) v.AddArg(x) return true } - // match: (SignExt8to64 x) - // result: (I64ShrS (I64Shl x (I64Const [56])) (I64Const [56])) - for { - x := v_0 - v.reset(OpWasmI64ShrS) - v0 := b.NewValue0(v.Pos, OpWasmI64Shl, typ.Int64) - v1 := b.NewValue0(v.Pos, OpWasmI64Const, typ.Int64) - v1.AuxInt = int64ToAuxInt(56) - v0.AddArg2(x, v1) - v.AddArg2(v0, v1) - return true - } } func rewriteValueWasm_OpSlicemask(v *Value) bool { v_0 := v.Args[0] diff --git a/src/cmd/compile/internal/wasm/ssa.go b/src/cmd/compile/internal/wasm/ssa.go index daee82f1fd7366..1e3b318e8c9fe0 100644 --- a/src/cmd/compile/internal/wasm/ssa.go +++ b/src/cmd/compile/internal/wasm/ssa.go @@ -14,7 +14,6 @@ import ( "cmd/compile/internal/types" "cmd/internal/obj" "cmd/internal/obj/wasm" - "internal/buildcfg" ) /* @@ -425,27 +424,11 @@ func ssaGenValueOnStack(s *ssagen.State, v *ssa.Value, extend bool) { case ssa.OpWasmI64TruncSatF32S, ssa.OpWasmI64TruncSatF64S: getValue64(s, v.Args[0]) - if buildcfg.GOWASM.SatConv { - s.Prog(v.Op.Asm()) - } else { - if v.Op == ssa.OpWasmI64TruncSatF32S { - s.Prog(wasm.AF64PromoteF32) - } - p := s.Prog(wasm.ACall) - p.To = obj.Addr{Type: obj.TYPE_MEM, Name: obj.NAME_EXTERN, Sym: ir.Syms.WasmTruncS} - } + s.Prog(v.Op.Asm()) case ssa.OpWasmI64TruncSatF32U, ssa.OpWasmI64TruncSatF64U: getValue64(s, v.Args[0]) - if buildcfg.GOWASM.SatConv { - s.Prog(v.Op.Asm()) - } else { - if v.Op == ssa.OpWasmI64TruncSatF32U { - s.Prog(wasm.AF64PromoteF32) - } - p := s.Prog(wasm.ACall) - p.To = obj.Addr{Type: obj.TYPE_MEM, Name: obj.NAME_EXTERN, Sym: ir.Syms.WasmTruncU} - } + s.Prog(v.Op.Asm()) case ssa.OpWasmF32DemoteF64: getValue64(s, v.Args[0]) diff --git a/src/internal/buildcfg/cfg.go b/src/internal/buildcfg/cfg.go index 9ab29568d22704..a75960b8e6c034 100644 --- a/src/internal/buildcfg/cfg.go +++ b/src/internal/buildcfg/cfg.go @@ -321,18 +321,13 @@ func goriscv64() int { } type gowasmFeatures struct { - SatConv bool - SignExt bool + // Legacy features, now always enabled + //SatConv bool + //SignExt bool } func (f gowasmFeatures) String() string { var flags []string - if f.SatConv { - flags = append(flags, "satconv") - } - if f.SignExt { - flags = append(flags, "signext") - } return strings.Join(flags, ",") } @@ -340,9 +335,9 @@ func gowasm() (f gowasmFeatures) { for opt := range strings.SplitSeq(envOr("GOWASM", ""), ",") { switch opt { case "satconv": - f.SatConv = true + // ignore, always enabled case "signext": - f.SignExt = true + // ignore, always enabled case "": // ignore default: @@ -452,12 +447,10 @@ func gogoarchTags() []string { return list case "wasm": var list []string - if GOWASM.SatConv { - list = append(list, GOARCH+".satconv") - } - if GOWASM.SignExt { - list = append(list, GOARCH+".signext") - } + // SatConv is always enabled + list = append(list, GOARCH+".satconv") + // SignExt is always enabled + list = append(list, GOARCH+".signext") return list } return nil diff --git a/src/runtime/conv_wasm_test.go b/src/runtime/conv_wasm_test.go index 5054fca04dc40a..3979a7b618028b 100644 --- a/src/runtime/conv_wasm_test.go +++ b/src/runtime/conv_wasm_test.go @@ -11,6 +11,8 @@ import ( var res int64 var ures uint64 +// TODO: This test probably should be in a different place. + func TestFloatTruncation(t *testing.T) { testdata := []struct { input float64 @@ -21,36 +23,37 @@ func TestFloatTruncation(t *testing.T) { // max +- 1 { input: 0x7fffffffffffffff, - convInt64: -0x8000000000000000, + convInt64: 0x7fffffffffffffff, convUInt64: 0x8000000000000000, }, // For out-of-bounds conversion, the result is implementation-dependent. - // This test verifies the implementation of wasm architecture. + // This test verifies the implementation of wasm architecture, which is, + // saturating to the min/max value. { input: 0x8000000000000000, - convInt64: -0x8000000000000000, + convInt64: 0x7fffffffffffffff, convUInt64: 0x8000000000000000, }, { input: 0x7ffffffffffffffe, - convInt64: -0x8000000000000000, + convInt64: 0x7fffffffffffffff, convUInt64: 0x8000000000000000, }, // neg max +- 1 { input: -0x8000000000000000, convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convUInt64: 0, }, { input: -0x8000000000000001, convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convUInt64: 0, }, { input: -0x7fffffffffffffff, convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convUInt64: 0, }, // trunc point +- 1 { @@ -60,7 +63,7 @@ func TestFloatTruncation(t *testing.T) { }, { input: 0x7ffffffffffffe00, - convInt64: -0x8000000000000000, + convInt64: 0x7fffffffffffffff, convUInt64: 0x8000000000000000, }, { @@ -72,48 +75,48 @@ func TestFloatTruncation(t *testing.T) { { input: -0x7ffffffffffffdff, convInt64: -0x7ffffffffffffc00, - convUInt64: 0x8000000000000000, + convUInt64: 0, }, { input: -0x7ffffffffffffe00, convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convUInt64: 0, }, { input: -0x7ffffffffffffdfe, convInt64: -0x7ffffffffffffc00, - convUInt64: 0x8000000000000000, + convUInt64: 0, }, // umax +- 1 { input: 0xffffffffffffffff, - convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convInt64: 0x7fffffffffffffff, + convUInt64: 0xffffffffffffffff, }, { input: 0x10000000000000000, - convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convInt64: 0x7fffffffffffffff, + convUInt64: 0xffffffffffffffff, }, { input: 0xfffffffffffffffe, - convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convInt64: 0x7fffffffffffffff, + convUInt64: 0xffffffffffffffff, }, // umax trunc +- 1 { input: 0xfffffffffffffbff, - convInt64: -0x8000000000000000, + convInt64: 0x7fffffffffffffff, convUInt64: 0xfffffffffffff800, }, { input: 0xfffffffffffffc00, - convInt64: -0x8000000000000000, - convUInt64: 0x8000000000000000, + convInt64: 0x7fffffffffffffff, + convUInt64: 0xffffffffffffffff, }, { input: 0xfffffffffffffbfe, - convInt64: -0x8000000000000000, + convInt64: 0x7fffffffffffffff, convUInt64: 0xfffffffffffff800, }, } diff --git a/src/runtime/sys_wasm.s b/src/runtime/sys_wasm.s index b7965ec3fa4138..95c162eb857ac4 100644 --- a/src/runtime/sys_wasm.s +++ b/src/runtime/sys_wasm.s @@ -22,64 +22,6 @@ TEXT runtime·wasmDiv(SB), NOSPLIT, $0-0 I64DivS Return -TEXT runtime·wasmTruncS(SB), NOSPLIT, $0-0 - Get R0 - Get R0 - F64Ne // NaN - If - I64Const $0x8000000000000000 - Return - End - - Get R0 - F64Const $0x7ffffffffffffc00p0 // Maximum truncated representation of 0x7fffffffffffffff - F64Gt - If - I64Const $0x8000000000000000 - Return - End - - Get R0 - F64Const $-0x7ffffffffffffc00p0 // Minimum truncated representation of -0x8000000000000000 - F64Lt - If - I64Const $0x8000000000000000 - Return - End - - Get R0 - I64TruncF64S - Return - -TEXT runtime·wasmTruncU(SB), NOSPLIT, $0-0 - Get R0 - Get R0 - F64Ne // NaN - If - I64Const $0x8000000000000000 - Return - End - - Get R0 - F64Const $0xfffffffffffff800p0 // Maximum truncated representation of 0xffffffffffffffff - F64Gt - If - I64Const $0x8000000000000000 - Return - End - - Get R0 - F64Const $0. - F64Lt - If - I64Const $0x8000000000000000 - Return - End - - Get R0 - I64TruncF64U - Return - TEXT runtime·exitThread(SB), NOSPLIT, $0-0 UNDEF From 6e95748335bdd074c5b48fe9c3af4ced18c388dd Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 12 Sep 2025 16:23:40 +0200 Subject: [PATCH 17/63] cmd/link/internal/arm64: support Mach-O ARM64_RELOC_POINTER_TO_GOT in internal linking ARM64_RELOC_POINTER_TO_GOT is the arm64 version of X86_64_RELOC_GOT, which has been support for many years now. The standard library still doesn't need it, but I've found it necessary when statically linking against a library I own. Change-Id: I8eb7bf3c74aed663a1fc00b5dd986057130f7f7a Reviewed-on: https://go-review.googlesource.com/c/go/+/703315 LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee Reviewed-by: Cherry Mui --- src/cmd/link/internal/arm64/asm.go | 11 +++++++++++ src/cmd/link/internal/ld/macho.go | 1 + 2 files changed, 12 insertions(+) diff --git a/src/cmd/link/internal/arm64/asm.go b/src/cmd/link/internal/arm64/asm.go index 68474b4484f1af..b2572fd1040b0b 100644 --- a/src/cmd/link/internal/arm64/asm.go +++ b/src/cmd/link/internal/arm64/asm.go @@ -277,6 +277,17 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade su.SetRelocSym(rIdx, syms.GOT) su.SetRelocAdd(rIdx, int64(ldr.SymGot(targ))) return true + + case objabi.MachoRelocOffset + ld.MACHO_ARM64_RELOC_POINTER_TO_GOT*2 + pcrel: + if targType != sym.SDYNIMPORT { + ldr.Errorf(s, "unexpected GOT reloc for non-dynamic symbol %s", ldr.SymName(targ)) + } + ld.AddGotSym(target, ldr, syms, targ, 0) + su := ldr.MakeSymbolUpdater(s) + su.SetRelocType(rIdx, objabi.R_PCREL) + su.SetRelocSym(rIdx, syms.GOT) + su.SetRelocAdd(rIdx, r.Add()+int64(r.Siz())+int64(ldr.SymGot(targ))) + return true } // Reread the reloc to incorporate any changes in type above. diff --git a/src/cmd/link/internal/ld/macho.go b/src/cmd/link/internal/ld/macho.go index 431dad9d6bcbaa..6920f42015b344 100644 --- a/src/cmd/link/internal/ld/macho.go +++ b/src/cmd/link/internal/ld/macho.go @@ -111,6 +111,7 @@ const ( MACHO_ARM64_RELOC_PAGEOFF12 = 4 MACHO_ARM64_RELOC_GOT_LOAD_PAGE21 = 5 MACHO_ARM64_RELOC_GOT_LOAD_PAGEOFF12 = 6 + MACHO_ARM64_RELOC_POINTER_TO_GOT = 7 MACHO_ARM64_RELOC_ADDEND = 10 MACHO_GENERIC_RELOC_VANILLA = 0 MACHO_FAKE_GOTPCREL = 100 From 7c8166d02d36a5dfcdbe3dd1b148412cceacf9f2 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 12 Sep 2025 16:31:01 +0200 Subject: [PATCH 18/63] cmd/link/internal/arm64: support Mach-O ARM64_RELOC_SUBTRACTOR in internal linking ARM64_RELOC_SUBTRACTOR is the arm64 version of X86_64_RELOC_SUBTRACTOR, which has been recently implemented in CL 660715. The standard library still doesn't need it, but I've found it necessary when statically linking against a library I own. Change-Id: I138281b12f2304e3673f7dc92f7137e48bf68fdd Reviewed-on: https://go-review.googlesource.com/c/go/+/703316 Reviewed-by: Carlos Amedee LUCI-TryBot-Result: Go LUCI Reviewed-by: Cherry Mui --- src/cmd/link/internal/arm64/asm.go | 22 ++++++++++++++++++++++ src/cmd/link/internal/ld/macho.go | 1 + 2 files changed, 23 insertions(+) diff --git a/src/cmd/link/internal/arm64/asm.go b/src/cmd/link/internal/arm64/asm.go index b2572fd1040b0b..8d8ea8ac542c50 100644 --- a/src/cmd/link/internal/arm64/asm.go +++ b/src/cmd/link/internal/arm64/asm.go @@ -224,6 +224,28 @@ func adddynrel(target *ld.Target, ldr *loader.Loader, syms *ld.ArchSyms, s loade } return true + case objabi.MachoRelocOffset + ld.MACHO_ARM64_RELOC_SUBTRACTOR*2: + // ARM64_RELOC_SUBTRACTOR must be followed by ARM64_RELOC_UNSIGNED. + // The pair of relocations resolves to the difference between two + // symbol addresses (each relocation specifies a symbol). + outer, off := ld.FoldSubSymbolOffset(ldr, targ) + if outer != s { + // TODO: support subtracted symbol in different section. + ldr.Errorf(s, "unsupported ARM64_RELOC_SUBTRACTOR reloc: target %s, outer %s", ldr.SymName(targ), ldr.SymName(outer)) + break + } + su := ldr.MakeSymbolUpdater(s) + relocs := su.Relocs() + if rIdx+1 >= relocs.Count() || relocs.At(rIdx+1).Type() != objabi.MachoRelocOffset+ld.MACHO_ARM64_RELOC_UNSIGNED*2 || relocs.At(rIdx+1).Off() != r.Off() { + ldr.Errorf(s, "unexpected ARM64_RELOC_SUBTRACTOR reloc, must be followed by ARM64_RELOC_UNSIGNED at same offset") + break + } + su.SetRelocType(rIdx+1, objabi.R_PCREL) + su.SetRelocAdd(rIdx+1, r.Add()+int64(r.Off())+int64(r.Siz())-off) + // Remove the other relocation + su.SetRelocSiz(rIdx, 0) + return true + case objabi.MachoRelocOffset + ld.MACHO_ARM64_RELOC_BRANCH26*2 + pcrel: su := ldr.MakeSymbolUpdater(s) su.SetRelocType(rIdx, objabi.R_CALLARM64) diff --git a/src/cmd/link/internal/ld/macho.go b/src/cmd/link/internal/ld/macho.go index 6920f42015b344..c26263466616f5 100644 --- a/src/cmd/link/internal/ld/macho.go +++ b/src/cmd/link/internal/ld/macho.go @@ -106,6 +106,7 @@ const ( MACHO_ARM_RELOC_SECTDIFF = 2 MACHO_ARM_RELOC_BR24 = 5 MACHO_ARM64_RELOC_UNSIGNED = 0 + MACHO_ARM64_RELOC_SUBTRACTOR = 1 MACHO_ARM64_RELOC_BRANCH26 = 2 MACHO_ARM64_RELOC_PAGE21 = 3 MACHO_ARM64_RELOC_PAGEOFF12 = 4 From a846bb0aa523c8781248161b63bc2ab6a245cec1 Mon Sep 17 00:00:00 2001 From: Julien Cretel Date: Mon, 29 Sep 2025 16:57:53 +0000 Subject: [PATCH 19/63] errors: add AsType Fixes #51945 Change-Id: Icda169782e796578eba728938134a85b5827d3b6 GitHub-Last-Rev: c6ff335ee1ffb6b7975141795a4632a55247299d GitHub-Pull-Request: golang/go#75621 Reviewed-on: https://go-review.googlesource.com/c/go/+/707235 Reviewed-by: Carlos Amedee Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Sean Liao --- api/next/51945.txt | 1 + doc/next/6-stdlib/99-minor/errors/51945.md | 2 + src/errors/errors.go | 10 +- src/errors/example_test.go | 12 ++ src/errors/wrap.go | 61 ++++++++++ src/errors/wrap_test.go | 126 +++++++++++++++++++++ 6 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 api/next/51945.txt create mode 100644 doc/next/6-stdlib/99-minor/errors/51945.md diff --git a/api/next/51945.txt b/api/next/51945.txt new file mode 100644 index 00000000000000..7db1f093e5a2a5 --- /dev/null +++ b/api/next/51945.txt @@ -0,0 +1 @@ +pkg errors, func AsType[$0 error](error) ($0, bool) #51945 diff --git a/doc/next/6-stdlib/99-minor/errors/51945.md b/doc/next/6-stdlib/99-minor/errors/51945.md new file mode 100644 index 00000000000000..44ac7222e6d990 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/errors/51945.md @@ -0,0 +1,2 @@ +The new [AsType] function is a generic version of [As]. It is type-safe, faster, +and, in most cases, easier to use. diff --git a/src/errors/errors.go b/src/errors/errors.go index 5059be12ed441f..8b926cfe148c44 100644 --- a/src/errors/errors.go +++ b/src/errors/errors.go @@ -41,12 +41,12 @@ // // because the former will succeed if err wraps [io/fs.ErrExist]. // -// [As] examines the tree of its first argument looking for an error that can be -// assigned to its second argument, which must be a pointer. If it succeeds, it -// performs the assignment and returns true. Otherwise, it returns false. The form +// [AsType] examines the tree of its argument looking for an error whose +// type matches its type argument. If it succeeds, it returns the +// corresponding value of that type and true. Otherwise, it returns the +// zero value of that type and false. The form // -// var perr *fs.PathError -// if errors.As(err, &perr) { +// if perr, ok := errors.AsType[*fs.PathError](err); ok { // fmt.Println(perr.Path) // } // diff --git a/src/errors/example_test.go b/src/errors/example_test.go index 278df8c7da6e5a..92ef36b1010edb 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -102,6 +102,18 @@ func ExampleAs() { // Failed at path: non-existing } +func ExampleAsType() { + if _, err := os.Open("non-existing"); err != nil { + if pathError, ok := errors.AsType[*fs.PathError](err); ok { + fmt.Println("Failed at path:", pathError.Path) + } else { + fmt.Println(err) + } + } + // Output: + // Failed at path: non-existing +} + func ExampleUnwrap() { err1 := errors.New("error1") err2 := fmt.Errorf("error2: [%w]", err1) diff --git a/src/errors/wrap.go b/src/errors/wrap.go index eec9591dae7b93..2ebb951f1de93d 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -80,6 +80,10 @@ func is(err, target error, targetComparable bool) bool { // As finds the first error in err's tree that matches target, and if one is found, sets // target to that error value and returns true. Otherwise, it returns false. // +// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its target +// argument rather than returning the matching error and doesn't require its target +// argument to implement error. +// // The tree consists of err itself, followed by the errors obtained by repeatedly // calling its Unwrap() error or Unwrap() []error method. When err wraps multiple // errors, As examines err followed by a depth-first traversal of its children. @@ -145,3 +149,60 @@ func as(err error, target any, targetVal reflectlite.Value, targetType reflectli } var errorType = reflectlite.TypeOf((*error)(nil)).Elem() + +// AsType finds the first error in err's tree that matches the type E, and +// if one is found, returns that error value and true. Otherwise, it +// returns the zero value of E and false. +// +// The tree consists of err itself, followed by the errors obtained by +// repeatedly calling its Unwrap() error or Unwrap() []error method. When +// err wraps multiple errors, AsType examines err followed by a +// depth-first traversal of its children. +// +// An error err matches the type E if the type assertion err.(E) holds, +// or if the error has a method As(any) bool such that err.As(target) +// returns true when target is a non-nil *E. In the latter case, the As +// method is responsible for setting target. +func AsType[E error](err error) (E, bool) { + if err == nil { + var zero E + return zero, false + } + var pe *E // lazily initialized + return asType(err, &pe) +} + +func asType[E error](err error, ppe **E) (_ E, _ bool) { + for { + if e, ok := err.(E); ok { + return e, true + } + if x, ok := err.(interface{ As(any) bool }); ok { + if *ppe == nil { + *ppe = new(E) + } + if x.As(*ppe) { + return **ppe, true + } + } + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if err == nil { + continue + } + if x, ok := asType(err, ppe); ok { + return x, true + } + } + return + default: + return + } + } +} diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go index 58ed95fd9a0fc7..81c795a6bb8b18 100644 --- a/src/errors/wrap_test.go +++ b/src/errors/wrap_test.go @@ -239,6 +239,123 @@ func TestAsValidation(t *testing.T) { } } +func TestAsType(t *testing.T) { + var errT errorT + var errP *fs.PathError + type timeout interface { + Timeout() bool + error + } + _, errF := os.Open("non-existing") + poserErr := &poser{"oh no", nil} + + testAsType(t, + nil, + errP, + false, + ) + testAsType(t, + wrapped{"pitied the fool", errorT{"T"}}, + errorT{"T"}, + true, + ) + testAsType(t, + errF, + errF, + true, + ) + testAsType(t, + errT, + errP, + false, + ) + testAsType(t, + wrapped{"wrapped", nil}, + errT, + false, + ) + testAsType(t, + &poser{"error", nil}, + errorT{"poser"}, + true, + ) + testAsType(t, + &poser{"path", nil}, + poserPathErr, + true, + ) + testAsType(t, + poserErr, + poserErr, + true, + ) + testAsType(t, + errors.New("err"), + timeout(nil), + false, + ) + testAsType(t, + errF, + errF.(timeout), + true) + testAsType(t, + wrapped{"path error", errF}, + errF.(timeout), + true, + ) + testAsType(t, + multiErr{}, + errT, + false, + ) + testAsType(t, + multiErr{errors.New("a"), errorT{"T"}}, + errorT{"T"}, + true, + ) + testAsType(t, + multiErr{errorT{"T"}, errors.New("a")}, + errorT{"T"}, + true, + ) + testAsType(t, + multiErr{errorT{"a"}, errorT{"b"}}, + errorT{"a"}, + true, + ) + testAsType(t, + multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}}, + errorT{"a"}, + true, + ) + testAsType(t, + multiErr{wrapped{"path error", errF}}, + errF.(timeout), + true, + ) + testAsType(t, + multiErr{nil}, + errT, + false, + ) +} + +type compError interface { + comparable + error +} + +func testAsType[E compError](t *testing.T, err error, want E, wantOK bool) { + t.Helper() + name := fmt.Sprintf("AsType[%T](Errorf(..., %v))", want, err) + t.Run(name, func(t *testing.T) { + got, gotOK := errors.AsType[E](err) + if gotOK != wantOK || got != want { + t.Fatalf("got %v, %t; want %v, %t", got, gotOK, want, wantOK) + } + }) +} + func BenchmarkIs(b *testing.B) { err1 := errors.New("1") err2 := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} @@ -260,6 +377,15 @@ func BenchmarkAs(b *testing.B) { } } +func BenchmarkAsType(b *testing.B) { + err := multiErr{multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}}} + for range b.N { + if _, ok := errors.AsType[errorT](err); !ok { + b.Fatal("AsType failed") + } + } +} + func TestUnwrap(t *testing.T) { err1 := errors.New("1") erra := wrapped{"wrap 2", err1} From 300d9d2714164e455abc7990d52de9de6b084df1 Mon Sep 17 00:00:00 2001 From: Steve Muir Date: Thu, 18 Sep 2025 07:54:57 -0700 Subject: [PATCH 20/63] runtime: initialise debug settings much earlier in startup process This is necessary specifically to set the value of `debug.decoratemappings` sufficiently early in the startup sequence that all memory ranges allocated can be named appropriately using the new Linux-specific naming API introduced in #71546. Example output (on ARM64): https://gist.github.com/9muir/3667654b9c3f52e8be92756219371672 Fixes: #75324 Change-Id: Ic0b16233f54a45adef1660c4d0df59af2f5af86a Reviewed-on: https://go-review.googlesource.com/c/go/+/703476 Auto-Submit: Michael Knyszek LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek --- src/runtime/decoratemappings_test.go | 72 ++++++++++++++++++++++++++++ src/runtime/export_test.go | 4 ++ src/runtime/proc.go | 24 ++++++++-- src/runtime/runtime1.go | 15 +++--- src/runtime/set_vma_name_linux.go | 6 ++- src/runtime/set_vma_name_stub.go | 2 + 6 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/runtime/decoratemappings_test.go diff --git a/src/runtime/decoratemappings_test.go b/src/runtime/decoratemappings_test.go new file mode 100644 index 00000000000000..7d1121c125df08 --- /dev/null +++ b/src/runtime/decoratemappings_test.go @@ -0,0 +1,72 @@ +// 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 runtime_test + +import ( + "os" + "regexp" + "runtime" + "testing" +) + +func validateMapLabels(t *testing.T, labels []string) { + // These are the specific region labels that need get added during the + // runtime phase. Hence they are the ones that need to be confirmed as + // present at the time the test reads its own region labels, which + // is sufficient to validate that the default `decoratemappings` value + // (enabled) was set early enough in the init process. + regions := map[string]bool{ + "allspans array": false, + "gc bits": false, + "heap": false, + "heap index": false, + "heap reservation": false, + "immortal metadata": false, + "page alloc": false, + "page alloc index": false, + "page summary": false, + "scavenge index": false, + } + for _, label := range labels { + if _, ok := regions[label]; !ok { + t.Logf("unexpected region label found: \"%s\"", label) + } + regions[label] = true + } + for label, found := range regions { + if !found { + t.Logf("region label missing: \"%s\"", label) + } + } +} + +func TestDecorateMappings(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("decoratemappings is only supported on Linux") + // /proc/self/maps is also Linux-specific + } + + var labels []string + if rawMaps, err := os.ReadFile("/proc/self/maps"); err != nil { + t.Fatalf("failed to read /proc/self/maps: %v", err) + } else { + t.Logf("maps:%s\n", string(rawMaps)) + matches := regexp.MustCompile("[^[]+ \\[anon: Go: (.+)\\]\n").FindAllSubmatch(rawMaps, -1) + for _, match_pair := range matches { + // match_pair consists of the matching substring and the parenthesized group + labels = append(labels, string(match_pair[1])) + } + } + t.Logf("DebugDecorateMappings: %v", *runtime.DebugDecorateMappings) + if *runtime.DebugDecorateMappings != 0 && runtime.SetVMANameSupported() { + validateMapLabels(t, labels) + } else { + if len(labels) > 0 { + t.Errorf("unexpected mapping labels present: %v", labels) + } else { + t.Skip("mapping labels absent as expected") + } + } +} diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go index 2a701115685411..9f2fcacc30ee5c 100644 --- a/src/runtime/export_test.go +++ b/src/runtime/export_test.go @@ -1936,3 +1936,7 @@ func (t *TraceStackTable) Reset() { func TraceStack(gp *G, tab *TraceStackTable) { traceStack(0, gp, (*traceStackTable)(tab)) } + +var DebugDecorateMappings = &debug.decoratemappings + +func SetVMANameSupported() bool { return setVMANameSupported() } diff --git a/src/runtime/proc.go b/src/runtime/proc.go index 2c42cad6c10e8a..4b1ab1af6a5075 100644 --- a/src/runtime/proc.go +++ b/src/runtime/proc.go @@ -789,7 +789,9 @@ func cpuinit(env string) { // getGodebugEarly extracts the environment variable GODEBUG from the environment on // Unix-like operating systems and returns it. This function exists to extract GODEBUG // early before much of the runtime is initialized. -func getGodebugEarly() string { +// +// Returns nil, false if OS doesn't provide env vars early in the init sequence. +func getGodebugEarly() (string, bool) { const prefix = "GODEBUG=" var env string switch GOOS { @@ -807,12 +809,16 @@ func getGodebugEarly() string { s := unsafe.String(p, findnull(p)) if stringslite.HasPrefix(s, prefix) { - env = gostring(p)[len(prefix):] + env = gostringnocopy(p)[len(prefix):] break } } + break + + default: + return "", false } - return env + return env, true } // The bootstrap sequence is: @@ -859,12 +865,15 @@ func schedinit() { // The world starts stopped. worldStopped() + godebug, parsedGodebug := getGodebugEarly() + if parsedGodebug { + parseRuntimeDebugVars(godebug) + } ticks.init() // run as early as possible moduledataverify() stackinit() randinit() // must run before mallocinit, alginit, mcommoninit mallocinit() - godebug := getGodebugEarly() cpuinit(godebug) // must run before alginit alginit() // maps, hash, rand must not be used before this call mcommoninit(gp.m, -1) @@ -880,7 +889,12 @@ func schedinit() { goenvs() secure() checkfds() - parsedebugvars() + if !parsedGodebug { + // Some platforms, e.g., Windows, didn't make env vars available "early", + // so try again now. + parseRuntimeDebugVars(gogetenv("GODEBUG")) + } + finishDebugVarsSetup() gcinit() // Allocate stack space that can be used when crashing due to bad stack diff --git a/src/runtime/runtime1.go b/src/runtime/runtime1.go index 424745d2357dc9..15b546783b5e53 100644 --- a/src/runtime/runtime1.go +++ b/src/runtime/runtime1.go @@ -402,7 +402,7 @@ var dbgvars = []*dbgVar{ {name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1}, } -func parsedebugvars() { +func parseRuntimeDebugVars(godebug string) { // defaults debug.cgocheck = 1 debug.invalidptr = 1 @@ -420,12 +420,6 @@ func parsedebugvars() { } debug.traceadvanceperiod = defaultTraceAdvancePeriod - godebug := gogetenv("GODEBUG") - - p := new(string) - *p = godebug - godebugEnv.Store(p) - // apply runtime defaults, if any for _, v := range dbgvars { if v.def != 0 { @@ -437,7 +431,6 @@ func parsedebugvars() { } } } - // apply compile-time GODEBUG settings parsegodebug(godebugDefault, nil) @@ -463,6 +456,12 @@ func parsedebugvars() { if debug.gccheckmark > 0 { debug.asyncpreemptoff = 1 } +} + +func finishDebugVarsSetup() { + p := new(string) + *p = gogetenv("GODEBUG") + godebugEnv.Store(p) setTraceback(gogetenv("GOTRACEBACK")) traceback_env = traceback_cache diff --git a/src/runtime/set_vma_name_linux.go b/src/runtime/set_vma_name_linux.go index 9b6654f33299a0..03c7739c3465c9 100644 --- a/src/runtime/set_vma_name_linux.go +++ b/src/runtime/set_vma_name_linux.go @@ -14,9 +14,13 @@ import ( var prSetVMAUnsupported atomic.Bool +func setVMANameSupported() bool { + return !prSetVMAUnsupported.Load() +} + // setVMAName calls prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, start, len, name) func setVMAName(start unsafe.Pointer, length uintptr, name string) { - if debug.decoratemappings == 0 || prSetVMAUnsupported.Load() { + if debug.decoratemappings == 0 || !setVMANameSupported() { return } diff --git a/src/runtime/set_vma_name_stub.go b/src/runtime/set_vma_name_stub.go index 38f65fd592ba34..6cb01ebf50dcef 100644 --- a/src/runtime/set_vma_name_stub.go +++ b/src/runtime/set_vma_name_stub.go @@ -10,3 +10,5 @@ import "unsafe" // setVMAName isn’t implemented func setVMAName(start unsafe.Pointer, len uintptr, name string) {} + +func setVMANameSupported() bool { return false } From 97da068774d5aa9147e63eb146350145c73bfc3d Mon Sep 17 00:00:00 2001 From: Jake Bailey Date: Sun, 7 Sep 2025 22:21:15 -0700 Subject: [PATCH 21/63] cmd/compile: eliminate nil checks on .dict arg The first arg of a generic function is the dictionary. This dictionary is never nil, but it gets a nil check becuase the dict arg is treated as a slice during construction. cmp.Compare[go.shape.int] was: 00006 (+41) TESTB AX, (AX) 00007 (+52) CMPQ CX, BX 00008 (52) JGT 14 00009 (+55) JGE 12 00010 (+56) MOVL $1, AX 00011 (56) RET 00012 (+58) XORL AX, AX 00013 (58) RET 00014 (+53) MOVQ $-1, AX 00015 (53) RET Note how the function begins with a TESTB that loads the dict to perform the nil check. This CL eliminates that nil check. For most generic functions, this doesn't matter too much, but not infrequently are generic functions written which never actually use the dictionary (like cmp.Compare), so I suspect this might help in hot code to avoid repeatedly touching the dictionary in memory, and in cases where the generic function is not inlined (and thus the dict dropped). compilecmp shows these changes (deduped): cmp.Compare[go.shape.float64] 73 -> 72 (-1.37%) cmp.Compare[go.shape.int] 26 -> 24 (-7.69%) cmp.Compare[go.shape.int32] 25 -> 23 (-8.00%) cmp.Compare[go.shape.int64] 26 -> 24 (-7.69%) cmp.Compare[go.shape.string] 142 -> 141 (-0.70%) cmp.Compare[go.shape.uint16] 26 -> 24 (-7.69%) cmp.Compare[go.shape.uint] 26 -> 24 (-7.69%) cmp.Compare[go.shape.uint32] 25 -> 23 (-8.00%) cmp.Compare[go.shape.uint64] 26 -> 24 (-7.69%) cmp.Compare[go.shape.uint8] 25 -> 23 (-8.00%) cmp.Compare[go.shape.uintptr] 26 -> 24 (-7.69%) cmp.Less[go.shape.float64] 35 -> 34 (-2.86%) cmp.Less[go.shape.int32] 8 -> 6 (-25.00%) cmp.Less[go.shape.int64] 9 -> 7 (-22.22%) cmp.Less[go.shape.int] 9 -> 7 (-22.22%) cmp.Less[go.shape.string] 112 -> 110 (-1.79%) cmp.Less[go.shape.uint16] 9 -> 7 (-22.22%) cmp.Less[go.shape.uint32] 8 -> 6 (-25.00%) cmp.Less[go.shape.uint64] 9 -> 7 (-22.22%) internal/synctest.Associate[go.shape.struct 114 -> 113 (-0.88%) internal/trace.(*dataTable[go.shape.uint64,go.shape.string]).insert 805 -> 791 (-1.74%) internal/trace.(*dataTable[go.shape.uint64,go.shape.struct 858 -> 852 (-0.70%) main.(*gState[go.shape.int64]).stop 2111 -> 2085 (-1.23%) main.(*gState[go.shape.int64]).unblock 941 -> 923 (-1.91%) runtime.fmax[go.shape.float32] 85 -> 83 (-2.35%) runtime.fmax[go.shape.float64] 89 -> 87 (-2.25%) runtime.fmin[go.shape.float32] 85 -> 83 (-2.35%) runtime.fmin[go.shape.float64] 89 -> 87 (-2.25%) slices.BinarySearch[go.shape.[]string,go.shape.string] 346 -> 337 (-2.60%) slices.Concat[go.shape.[]uint8,go.shape.uint8] 462 -> 453 (-1.95%) slices.ContainsFunc[go.shape.[]*cmd/vendor/github.com/google/pprof/profile.Sample,go.shape.*uint8] 170 -> 169 (-0.59%) slices.ContainsFunc[go.shape.[]*debug/dwarf.StructField,go.shape.*uint8] 170 -> 169 (-0.59%) slices.ContainsFunc[go.shape.[]*go/ast.Field,go.shape.*uint8] 170 -> 169 (-0.59%) slices.ContainsFunc[go.shape.[]string,go.shape.string] 186 -> 181 (-2.69%) slices.Contains[go.shape.[]*cmd/compile/internal/syntax.BranchStmt,go.shape.*cmd/compile/internal/syntax.BranchStmt] 44 -> 42 (-4.55%) slices.Contains[go.shape.[]cmd/compile/internal/syntax.Type,go.shape.interface 223 -> 219 (-1.79%) slices.Contains[go.shape.[]crypto/tls.CurveID,go.shape.uint16] 44 -> 42 (-4.55%) slices.Contains[go.shape.[]crypto/tls.SignatureScheme,go.shape.uint16] 44 -> 42 (-4.55%) slices.Contains[go.shape.[]*go/ast.BranchStmt,go.shape.*go/ast.BranchStmt] 44 -> 42 (-4.55%) slices.Contains[go.shape.[]go/types.Type,go.shape.interface 223 -> 219 (-1.79%) slices.Contains[go.shape.[]int,go.shape.int] 44 -> 42 (-4.55%) slices.Contains[go.shape.[]string,go.shape.string] 223 -> 219 (-1.79%) slices.Contains[go.shape.[]uint16,go.shape.uint16] 44 -> 42 (-4.55%) slices.Contains[go.shape.[]uint8,go.shape.uint8] 44 -> 42 (-4.55%) slices.Insert[go.shape.[]string,go.shape.string] 1189 -> 1170 (-1.60%) slices.medianCmpFunc[go.shape.struct 1118 -> 1113 (-0.45%) slices.medianCmpFunc[go.shape.struct 1214 -> 1209 (-0.41%) slices.medianCmpFunc[go.shape.struct 889 -> 887 (-0.22%) slices.medianCmpFunc[go.shape.struct 901 -> 874 (-3.00%) slices.order2Ordered[go.shape.float64] 89 -> 87 (-2.25%) slices.order2Ordered[go.shape.uint16] 75 -> 70 (-6.67%) slices.partialInsertionSortOrdered[go.shape.string] 1115 -> 1110 (-0.45%) slices.partialInsertionSortOrdered[go.shape.uint16] 358 -> 352 (-1.68%) slices.partitionEqualOrdered[go.shape.int] 208 -> 203 (-2.40%) slices.partitionEqualOrdered[go.shape.int32] 208 -> 198 (-4.81%) slices.partitionEqualOrdered[go.shape.int64] 208 -> 203 (-2.40%) slices.partitionEqualOrdered[go.shape.uint32] 208 -> 198 (-4.81%) slices.partitionEqualOrdered[go.shape.uint64] 208 -> 203 (-2.40%) slices.partitionOrdered[go.shape.float64] 538 -> 533 (-0.93%) slices.partitionOrdered[go.shape.int] 437 -> 427 (-2.29%) slices.partitionOrdered[go.shape.int64] 437 -> 427 (-2.29%) slices.partitionOrdered[go.shape.uint16] 447 -> 442 (-1.12%) slices.partitionOrdered[go.shape.uint64] 437 -> 427 (-2.29%) slices.rotateCmpFunc[go.shape.struct 1045 -> 1029 (-1.53%) slices.rotateCmpFunc[go.shape.struct 1205 -> 1163 (-3.49%) slices.rotateCmpFunc[go.shape.struct 1226 -> 1176 (-4.08%) slices.rotateCmpFunc[go.shape.struct 1322 -> 1272 (-3.78%) slices.rotateCmpFunc[go.shape.struct 1419 -> 1400 (-1.34%) slices.rotateCmpFunc[go.shape.*uint8] 549 -> 538 (-2.00%) slices.rotateLeft[go.shape.string] 603 -> 588 (-2.49%) slices.rotateLeft[go.shape.uint8] 255 -> 250 (-1.96%) slices.siftDownOrdered[go.shape.int] 181 -> 171 (-5.52%) slices.siftDownOrdered[go.shape.int32] 181 -> 171 (-5.52%) slices.siftDownOrdered[go.shape.int64] 181 -> 171 (-5.52%) slices.siftDownOrdered[go.shape.string] 614 -> 592 (-3.58%) slices.siftDownOrdered[go.shape.uint32] 181 -> 171 (-5.52%) slices.siftDownOrdered[go.shape.uint64] 181 -> 171 (-5.52%) time.parseRFC3339[go.shape.string] 1774 -> 1758 (-0.90%) unique.(*canonMap[go.shape.struct 280 -> 276 (-1.43%) unique.clone[go.shape.struct 311 -> 293 (-5.79%) weak.Make[go.shape.6880e4598856efac32416085c0172278cf0fb9e5050ce6518bd9b7f7d1662440] 136 -> 134 (-1.47%) weak.Make[go.shape.struct 136 -> 134 (-1.47%) weak.Make[go.shape.uint8] 136 -> 134 (-1.47%) Change-Id: I43dcea5f2aa37372f773e5edc6a2ef1dee0a8db7 Reviewed-on: https://go-review.googlesource.com/c/go/+/706655 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase Reviewed-by: Keith Randall Reviewed-by: Keith Randall Auto-Submit: Keith Randall --- .../compile/internal/ssa/_gen/generic.rules | 3 ++ src/cmd/compile/internal/ssa/rewrite.go | 6 +++ .../compile/internal/ssa/rewritegeneric.go | 15 +++++++ test/codegen/generics.go | 40 +++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 test/codegen/generics.go diff --git a/src/cmd/compile/internal/ssa/_gen/generic.rules b/src/cmd/compile/internal/ssa/_gen/generic.rules index 58872ca85a3961..b16aa473cd5961 100644 --- a/src/cmd/compile/internal/ssa/_gen/generic.rules +++ b/src/cmd/compile/internal/ssa/_gen/generic.rules @@ -2079,6 +2079,9 @@ && warnRule(fe.Debug_checknil(), v, "removed nil check") => ptr +// .dict args are always non-nil. +(NilCheck ptr:(Arg {sym}) _) && isDictArgSym(sym) => ptr + // Nil checks of nil checks are redundant. // See comment at the end of https://go-review.googlesource.com/c/go/+/537775. (NilCheck ptr:(NilCheck _ _) _ ) => ptr diff --git a/src/cmd/compile/internal/ssa/rewrite.go b/src/cmd/compile/internal/ssa/rewrite.go index 6d83ba565317a3..880c2223ef2229 100644 --- a/src/cmd/compile/internal/ssa/rewrite.go +++ b/src/cmd/compile/internal/ssa/rewrite.go @@ -6,9 +6,11 @@ package ssa import ( "cmd/compile/internal/base" + "cmd/compile/internal/ir" "cmd/compile/internal/logopt" "cmd/compile/internal/reflectdata" "cmd/compile/internal/rttype" + "cmd/compile/internal/typecheck" "cmd/compile/internal/types" "cmd/internal/obj" "cmd/internal/obj/s390x" @@ -2744,3 +2746,7 @@ func panicBoundsCToAux(p PanicBoundsC) Aux { func panicBoundsCCToAux(p PanicBoundsCC) Aux { return p } + +func isDictArgSym(sym Sym) bool { + return sym.(*ir.Name).Sym().Name == typecheck.LocalDictName +} diff --git a/src/cmd/compile/internal/ssa/rewritegeneric.go b/src/cmd/compile/internal/ssa/rewritegeneric.go index 7e23194e6aba12..5e0135be3a5be5 100644 --- a/src/cmd/compile/internal/ssa/rewritegeneric.go +++ b/src/cmd/compile/internal/ssa/rewritegeneric.go @@ -21393,6 +21393,21 @@ func rewriteValuegeneric_OpNilCheck(v *Value) bool { v.copyOf(ptr) return true } + // match: (NilCheck ptr:(Arg {sym}) _) + // cond: isDictArgSym(sym) + // result: ptr + for { + ptr := v_0 + if ptr.Op != OpArg { + break + } + sym := auxToSym(ptr.Aux) + if !(isDictArgSym(sym)) { + break + } + v.copyOf(ptr) + return true + } // match: (NilCheck ptr:(NilCheck _ _) _ ) // result: ptr for { diff --git a/test/codegen/generics.go b/test/codegen/generics.go new file mode 100644 index 00000000000000..45c4ca8d6aee61 --- /dev/null +++ b/test/codegen/generics.go @@ -0,0 +1,40 @@ +// asmcheck + +// 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 codegen + +import "cmp" + +func isNaN[T cmp.Ordered](x T) bool { + return x != x +} + +func compare[T cmp.Ordered](x, y T) int { + // amd64:-"TESTB" + // arm64:-"MOVB" + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN { + if yNaN { + return 0 + } + return -1 + } + if yNaN { + return +1 + } + if x < y { + return -1 + } + if x > y { + return +1 + } + return 0 +} + +func usesCompare(a, b int) int { + return compare(a, b) +} From 08afc50bea9a94e86adfc8cd852c6ae5b698cdaa Mon Sep 17 00:00:00 2001 From: Aidan Welch Date: Thu, 25 Sep 2025 21:00:45 +0000 Subject: [PATCH 22/63] mime: extend "builtinTypes" to include a more complete list of common types Implement all agreed upon types, using IANA's listed media types to decide when there is a disagreement in type. Except in the case of `.wav` where `audio/wav` is used. Fixes #69530 Change-Id: Iec99a6ceb534073be83c8390f48799bec3e4cfc7 GitHub-Last-Rev: e314c5ec6d9aba753dca5f6dbb9d1741bac43227 GitHub-Pull-Request: golang/go#69533 Reviewed-on: https://go-review.googlesource.com/c/go/+/614376 Reviewed-by: Damien Neil Reviewed-by: Emmanuel Odeke Auto-Submit: Sean Liao Reviewed-by: Sean Liao LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- src/mime/type.go | 93 +++++++++++++++++++++++++++++++++++-------- src/mime/type_test.go | 46 ++++++++++++++++++++- 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/src/mime/type.go b/src/mime/type.go index c86ebd3442c1dc..ac7b0447da3cf9 100644 --- a/src/mime/type.go +++ b/src/mime/type.go @@ -17,7 +17,7 @@ var ( mimeTypesLower sync.Map // map[string]string; ".z" => "application/x-compress" // extensions maps from MIME type to list of lowercase file - // extensions: "image/jpeg" => [".jpg", ".jpeg"] + // extensions: "image/jpeg" => [".jfif", ".jpg", ".jpeg", ".pjp", ".pjpeg"] extensionsMu sync.Mutex // Guards stores (but not loads) on extensions. extensions sync.Map // map[string][]string; slice values are append-only. ) @@ -50,23 +50,82 @@ func setMimeTypes(lowerExt, mixExt map[string]string) { } } +// A type is listed here if both Firefox and Chrome included them in their own +// lists. In the case where they contradict they are deconflicted using IANA's +// listed media types https://www.iana.org/assignments/media-types/media-types.xhtml +// +// Chrome's MIME mappings to file extensions are defined at +// https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/mime_util.cc +// +// Firefox's MIME types can be found at +// https://github.com/mozilla-firefox/firefox/blob/main/netwerk/mime/nsMimeTypes.h +// and the mappings to file extensions at +// https://github.com/mozilla-firefox/firefox/blob/main/uriloader/exthandler/nsExternalHelperAppService.cpp var builtinTypesLower = map[string]string{ - ".avif": "image/avif", - ".css": "text/css; charset=utf-8", - ".gif": "image/gif", - ".htm": "text/html; charset=utf-8", - ".html": "text/html; charset=utf-8", - ".jpeg": "image/jpeg", - ".jpg": "image/jpeg", - ".js": "text/javascript; charset=utf-8", - ".json": "application/json", - ".mjs": "text/javascript; charset=utf-8", - ".pdf": "application/pdf", - ".png": "image/png", - ".svg": "image/svg+xml", - ".wasm": "application/wasm", - ".webp": "image/webp", - ".xml": "text/xml; charset=utf-8", + ".ai": "application/postscript", + ".apk": "application/vnd.android.package-archive", + ".apng": "image/apng", + ".avif": "image/avif", + ".bin": "application/octet-stream", + ".bmp": "image/bmp", + ".com": "application/octet-stream", + ".css": "text/css; charset=utf-8", + ".csv": "text/csv; charset=utf-8", + ".doc": "application/msword", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".ehtml": "text/html; charset=utf-8", + ".eml": "message/rfc822", + ".eps": "application/postscript", + ".exe": "application/octet-stream", + ".flac": "audio/flac", + ".gif": "image/gif", + ".gz": "application/gzip", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".ico": "image/vnd.microsoft.icon", + ".ics": "text/calendar; charset=utf-8", + ".jfif": "image/jpeg", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json", + ".m4a": "audio/mp4", + ".mjs": "text/javascript; charset=utf-8", + ".mp3": "audio/mpeg", + ".mp4": "video/mp4", + ".oga": "audio/ogg", + ".ogg": "audio/ogg", + ".ogv": "video/ogg", + ".opus": "audio/ogg", + ".pdf": "application/pdf", + ".pjp": "image/jpeg", + ".pjpeg": "image/jpeg", + ".png": "image/png", + ".ppt": "application/vnd.ms-powerpoint", + ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".ps": "application/postscript", + ".rdf": "application/rdf+xml", + ".rtf": "application/rtf", + ".shtml": "text/html; charset=utf-8", + ".svg": "image/svg+xml", + ".text": "text/plain; charset=utf-8", + ".tif": "image/tiff", + ".tiff": "image/tiff", + ".txt": "text/plain; charset=utf-8", + ".vtt": "text/vtt; charset=utf-8", + ".wasm": "application/wasm", + ".wav": "audio/wav", + ".webm": "audio/webm", + ".webp": "image/webp", + ".xbl": "text/xml; charset=utf-8", + ".xbm": "image/x-xbitmap", + ".xht": "application/xhtml+xml", + ".xhtml": "application/xhtml+xml", + ".xls": "application/vnd.ms-excel", + ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xml": "text/xml; charset=utf-8", + ".xsl": "text/xml; charset=utf-8", + ".zip": "application/zip", } var once sync.Once // guards initMime diff --git a/src/mime/type_test.go b/src/mime/type_test.go index 6bdf37b6359d20..f4ec8c8754e69f 100644 --- a/src/mime/type_test.go +++ b/src/mime/type_test.go @@ -208,7 +208,51 @@ func TestExtensionsByType2(t *testing.T) { typ string want []string }{ - {typ: "image/jpeg", want: []string{".jpeg", ".jpg"}}, + {typ: "application/postscript", want: []string{".ai", ".eps", ".ps"}}, + {typ: "application/vnd.android.package-archive", want: []string{".apk"}}, + {typ: "image/apng", want: []string{".apng"}}, + {typ: "image/avif", want: []string{".avif"}}, + {typ: "application/octet-stream", want: []string{".bin", ".com", ".exe"}}, + {typ: "image/bmp", want: []string{".bmp"}}, + {typ: "text/css; charset=utf-8", want: []string{".css"}}, + {typ: "text/csv; charset=utf-8", want: []string{".csv"}}, + {typ: "application/msword", want: []string{".doc"}}, + {typ: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", want: []string{".docx"}}, + {typ: "text/html; charset=utf-8", want: []string{".ehtml", ".htm", ".html", ".shtml"}}, + {typ: "message/rfc822", want: []string{".eml"}}, + {typ: "audio/flac", want: []string{".flac"}}, + {typ: "image/gif", want: []string{".gif"}}, + {typ: "application/gzip", want: []string{".gz"}}, + {typ: "image/vnd.microsoft.icon", want: []string{".ico"}}, + {typ: "text/calendar; charset=utf-8", want: []string{".ics"}}, + {typ: "image/jpeg", want: []string{".jfif", ".jpeg", ".jpg", ".pjp", ".pjpeg"}}, + {typ: "text/javascript; charset=utf-8", want: []string{".js", ".mjs"}}, + {typ: "application/json", want: []string{".json"}}, + {typ: "audio/mp4", want: []string{".m4a"}}, + {typ: "audio/mpeg", want: []string{".mp3"}}, + {typ: "video/mp4", want: []string{".mp4"}}, + {typ: "audio/ogg", want: []string{".oga", ".ogg", ".opus"}}, + {typ: "video/ogg", want: []string{".ogv"}}, + {typ: "application/pdf", want: []string{".pdf"}}, + {typ: "image/png", want: []string{".png"}}, + {typ: "application/vnd.ms-powerpoint", want: []string{".ppt"}}, + {typ: "application/vnd.openxmlformats-officedocument.presentationml.presentation", want: []string{".pptx"}}, + {typ: "application/rdf+xml", want: []string{".rdf"}}, + {typ: "application/rtf", want: []string{".rtf"}}, + {typ: "image/svg+xml", want: []string{".svg"}}, + {typ: "text/plain; charset=utf-8", want: []string{".text", ".txt"}}, + {typ: "image/tiff", want: []string{".tif", ".tiff"}}, + {typ: "text/vtt; charset=utf-8", want: []string{".vtt"}}, + {typ: "application/wasm", want: []string{".wasm"}}, + {typ: "audio/wav", want: []string{".wav"}}, + {typ: "audio/webm", want: []string{".webm"}}, + {typ: "image/webp", want: []string{".webp"}}, + {typ: "text/xml; charset=utf-8", want: []string{".xbl", ".xml", ".xsl"}}, + {typ: "image/x-xbitmap", want: []string{".xbm"}}, + {typ: "application/xhtml+xml", want: []string{".xht", ".xhtml"}}, + {typ: "application/vnd.ms-excel", want: []string{".xls"}}, + {typ: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", want: []string{".xlsx"}}, + {typ: "application/zip", want: []string{".zip"}}, } for _, tt := range tests { From 19cc1022ba4e9ddf172c04107fa613e6d50a7eba Mon Sep 17 00:00:00 2001 From: Julien Cretel Date: Mon, 22 Sep 2025 18:05:12 +0000 Subject: [PATCH 23/63] mime: reduce allocs incurred by ParseMediaType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change is mostly gardening. It simplifies ParseMediaType and its helper functions and reduces the amount of allocations they incur. Here are some benchmark results: goos: darwin goarch: amd64 pkg: mime cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz │ old │ new │ │ sec/op │ sec/op vs base │ ParseMediaType-8 55.26µ ± 1% 54.54µ ± 1% -1.30% (p=0.000 n=20) ParseMediaTypeBogus-8 3.551µ ± 0% 3.427µ ± 0% -3.48% (p=0.000 n=20) geomean 14.01µ 13.67µ -2.39% │ old │ new │ │ B/op │ B/op vs base │ ParseMediaType-8 38.48Ki ± 0% 37.38Ki ± 0% -2.85% (p=0.000 n=20) ParseMediaTypeBogus-8 2.531Ki ± 0% 2.469Ki ± 0% -2.47% (p=0.000 n=20) geomean 9.869Ki 9.606Ki -2.66% │ old │ new │ │ allocs/op │ allocs/op vs base │ ParseMediaType-8 457.0 ± 0% 425.0 ± 0% -7.00% (p=0.000 n=20) ParseMediaTypeBogus-8 25.00 ± 0% 21.00 ± 0% -16.00% (p=0.000 n=20) geomean 106.9 94.47 -11.62% Change-Id: I51198b40396afa51531794a57c50aa88975eae1d GitHub-Last-Rev: c44e2a2577386d1d776498d29e31821326e20b92 GitHub-Pull-Request: golang/go#75565 Reviewed-on: https://go-review.googlesource.com/c/go/+/705715 Reviewed-by: Emmanuel Odeke Reviewed-by: Carlos Amedee LUCI-TryBot-Result: Go LUCI Reviewed-by: Sean Liao Reviewed-by: Damien Neil Auto-Submit: Emmanuel Odeke --- src/mime/mediatype.go | 67 ++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/mime/mediatype.go b/src/mime/mediatype.go index 66684a68b23961..c6006b614f319e 100644 --- a/src/mime/mediatype.go +++ b/src/mime/mediatype.go @@ -98,24 +98,32 @@ func FormatMediaType(t string, param map[string]string) string { func checkMediaTypeDisposition(s string) error { typ, rest := consumeToken(s) if typ == "" { - return errors.New("mime: no media type") + return errNoMediaType } if rest == "" { return nil } - if !strings.HasPrefix(rest, "/") { - return errors.New("mime: expected slash after first token") + var ok bool + if rest, ok = strings.CutPrefix(rest, "/"); !ok { + return errNoSlashAfterFirstToken } - subtype, rest := consumeToken(rest[1:]) + subtype, rest := consumeToken(rest) if subtype == "" { - return errors.New("mime: expected token after slash") + return errNoTokenAfterSlash } if rest != "" { - return errors.New("mime: unexpected content after media subtype") + return errUnexpectedContentAfterMediaSubtype } return nil } +var ( + errNoMediaType = errors.New("mime: no media type") + errNoSlashAfterFirstToken = errors.New("mime: expected slash after first token") + errNoTokenAfterSlash = errors.New("mime: expected token after slash") + errUnexpectedContentAfterMediaSubtype = errors.New("mime: unexpected content after media subtype") +) + // ErrInvalidMediaParameter is returned by [ParseMediaType] if // the media type value was found but there was an error parsing // the optional parameters @@ -169,7 +177,6 @@ func ParseMediaType(v string) (mediatype string, params map[string]string, err e if continuation == nil { continuation = make(map[string]map[string]string) } - var ok bool if pmap, ok = continuation[baseName]; !ok { continuation[baseName] = make(map[string]string) pmap = continuation[baseName] @@ -177,7 +184,7 @@ func ParseMediaType(v string) (mediatype string, params map[string]string, err e } if v, exists := pmap[key]; exists && v != value { // Duplicate parameter names are incorrect, but we allow them if they are equal. - return "", nil, errors.New("mime: duplicate parameter name") + return "", nil, errDuplicateParamName } pmap[key] = value v = rest @@ -227,27 +234,28 @@ func ParseMediaType(v string) (mediatype string, params map[string]string, err e return } +var errDuplicateParamName = errors.New("mime: duplicate parameter name") + func decode2231Enc(v string) (string, bool) { - sv := strings.SplitN(v, "'", 3) - if len(sv) != 3 { + charset, v, ok := strings.Cut(v, "'") + if !ok { return "", false } - // TODO: ignoring lang in sv[1] for now. If anybody needs it we'll + // TODO: ignoring the language part for now. If anybody needs it, we'll // need to decide how to expose it in the API. But I'm not sure // anybody uses it in practice. - charset := strings.ToLower(sv[0]) - if len(charset) == 0 { + _, extOtherVals, ok := strings.Cut(v, "'") + if !ok { return "", false } - if charset != "us-ascii" && charset != "utf-8" { - // TODO: unsupported encoding + charset = strings.ToLower(charset) + switch charset { + case "us-ascii", "utf-8": + default: + // Empty or unsupported encoding. return "", false } - encv, err := percentHexUnescape(sv[2]) - if err != nil { - return "", false - } - return encv, true + return percentHexUnescape(extOtherVals) } // consumeToken consumes a token from the beginning of provided @@ -309,11 +317,11 @@ func consumeValue(v string) (value, rest string) { func consumeMediaParam(v string) (param, value, rest string) { rest = strings.TrimLeftFunc(v, unicode.IsSpace) - if !strings.HasPrefix(rest, ";") { + var ok bool + if rest, ok = strings.CutPrefix(rest, ";"); !ok { return "", "", v } - rest = rest[1:] // consume semicolon rest = strings.TrimLeftFunc(rest, unicode.IsSpace) param, rest = consumeToken(rest) param = strings.ToLower(param) @@ -322,10 +330,9 @@ func consumeMediaParam(v string) (param, value, rest string) { } rest = strings.TrimLeftFunc(rest, unicode.IsSpace) - if !strings.HasPrefix(rest, "=") { + if rest, ok = strings.CutPrefix(rest, "="); !ok { return "", "", v } - rest = rest[1:] // consume equals sign rest = strings.TrimLeftFunc(rest, unicode.IsSpace) value, rest2 := consumeValue(rest) if value == "" && rest2 == rest { @@ -335,7 +342,7 @@ func consumeMediaParam(v string) (param, value, rest string) { return param, value, rest } -func percentHexUnescape(s string) (string, error) { +func percentHexUnescape(s string) (string, bool) { // Count %, check that they're well-formed. percents := 0 for i := 0; i < len(s); { @@ -345,16 +352,12 @@ func percentHexUnescape(s string) (string, error) { } percents++ if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { - s = s[i:] - if len(s) > 3 { - s = s[0:3] - } - return "", fmt.Errorf("mime: bogus characters after %%: %q", s) + return "", false } i += 3 } if percents == 0 { - return s, nil + return s, true } t := make([]byte, len(s)-2*percents) @@ -371,7 +374,7 @@ func percentHexUnescape(s string) (string, error) { i++ } } - return string(t), nil + return string(t), true } func ishex(c byte) bool { From fcb893fc4b615774f8cdd050e17ad227998e512a Mon Sep 17 00:00:00 2001 From: Youlin Feng Date: Fri, 5 Sep 2025 22:48:48 +0800 Subject: [PATCH 24/63] cmd/compile/internal/ssa: remove redundant "type:" prefix check Remove redundant "type:" prefix check on symbol names in isFixedLoad, also refactor some duplicate code into methods. Change-Id: I8358422596eea8c39d1a30a554bd0aae8b570038 Reviewed-on: https://go-review.googlesource.com/c/go/+/701275 Reviewed-by: Keith Randall Reviewed-by: Keith Randall Auto-Submit: Keith Randall LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- src/cmd/compile/internal/ssa/rewrite.go | 18 ++++++------------ src/cmd/internal/obj/link.go | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/cmd/compile/internal/ssa/rewrite.go b/src/cmd/compile/internal/ssa/rewrite.go index 880c2223ef2229..47f225c7aeb7c0 100644 --- a/src/cmd/compile/internal/ssa/rewrite.go +++ b/src/cmd/compile/internal/ssa/rewrite.go @@ -2059,12 +2059,12 @@ func isFixedLoad(v *Value, sym Sym, off int64) bool { return false } - if strings.HasPrefix(lsym.Name, "type:") { + if ti := lsym.TypeInfo(); ti != nil { // Type symbols do not contain information about their fields, unlike the cases above. // Hand-implement field accesses. // TODO: can this be replaced with reflectdata.writeType and just use the code above? - t := (*lsym.Extra).(*obj.TypeInfo).Type.(*types.Type) + t := ti.Type.(*types.Type) for _, f := range rttype.Type.Fields() { if f.Offset == off && copyCompatibleType(v.Type, f.Type) { @@ -2118,12 +2118,12 @@ func rewriteFixedLoad(v *Value, sym Sym, sb *Value, off int64) *Value { base.Fatalf("fixedLoad data not known for %s:%d", sym, off) } - if strings.HasPrefix(lsym.Name, "type:") { + if ti := lsym.TypeInfo(); ti != nil { // Type symbols do not contain information about their fields, unlike the cases above. // Hand-implement field accesses. // TODO: can this be replaced with reflectdata.writeType and just use the code above? - t := (*lsym.Extra).(*obj.TypeInfo).Type.(*types.Type) + t := ti.Type.(*types.Type) ptrSizedOpConst := OpConst64 if f.Config.PtrSize == 4 { @@ -2613,10 +2613,7 @@ func isDirectType1(v *Value) bool { return isDirectType2(v.Args[0]) case OpAddr: lsym := v.Aux.(*obj.LSym) - if lsym.Extra == nil { - return false - } - if ti, ok := (*lsym.Extra).(*obj.TypeInfo); ok { + if ti := lsym.TypeInfo(); ti != nil { return types.IsDirectIface(ti.Type.(*types.Type)) } } @@ -2649,10 +2646,7 @@ func isDirectIface1(v *Value, depth int) bool { return isDirectIface2(v.Args[0], depth-1) case OpAddr: lsym := v.Aux.(*obj.LSym) - if lsym.Extra == nil { - return false - } - if ii, ok := (*lsym.Extra).(*obj.ItabInfo); ok { + if ii := lsym.ItabInfo(); ii != nil { return types.IsDirectIface(ii.Type.(*types.Type)) } case OpConstNil: diff --git a/src/cmd/internal/obj/link.go b/src/cmd/internal/obj/link.go index 6513e116872a0a..816fed026f35ad 100644 --- a/src/cmd/internal/obj/link.go +++ b/src/cmd/internal/obj/link.go @@ -464,7 +464,7 @@ type LSym struct { P []byte R []Reloc - Extra *interface{} // *FuncInfo, *VarInfo, *FileInfo, or *TypeInfo, if present + Extra *interface{} // *FuncInfo, *VarInfo, *FileInfo, *TypeInfo, or *ItabInfo, if present Pkg string PkgIdx int32 @@ -604,6 +604,15 @@ func (s *LSym) NewTypeInfo() *TypeInfo { return t } +// TypeInfo returns the *TypeInfo associated with s, or else nil. +func (s *LSym) TypeInfo() *TypeInfo { + if s.Extra == nil { + return nil + } + t, _ := (*s.Extra).(*TypeInfo) + return t +} + // An ItabInfo contains information for a symbol // that contains a runtime.itab. type ItabInfo struct { @@ -620,6 +629,15 @@ func (s *LSym) NewItabInfo() *ItabInfo { return t } +// ItabInfo returns the *ItabInfo associated with s, or else nil. +func (s *LSym) ItabInfo() *ItabInfo { + if s.Extra == nil { + return nil + } + i, _ := (*s.Extra).(*ItabInfo) + return i +} + // WasmImport represents a WebAssembly (WASM) imported function with // parameters and results translated into WASM types based on the Go function // declaration. From 4ff8a457dbe68388a6e2451c78c7ea615f570cda Mon Sep 17 00:00:00 2001 From: Joel Sing Date: Mon, 15 Sep 2025 20:10:05 +1000 Subject: [PATCH 25/63] test/codegen: codify handling of floating point constants on arm64 While here, reorder Float32ConstantStore/Float64ConstantStore for consistency. Change-Id: Ic1b3e9f9474965d15bc94518d78d1a4a7bda93f3 Reviewed-on: https://go-review.googlesource.com/c/go/+/703756 Reviewed-by: Keith Randall Auto-Submit: Keith Randall LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee Auto-Submit: Joel Sing Reviewed-by: Keith Randall --- test/codegen/floats.go | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/test/codegen/floats.go b/test/codegen/floats.go index 29969c8dc08eaa..666c983b56ac04 100644 --- a/test/codegen/floats.go +++ b/test/codegen/floats.go @@ -217,14 +217,36 @@ func Float32Max(a, b float32) float32 { // Constant Optimizations // // ------------------------ // +func Float32ConstantZero() float32 { + // arm64:"FMOVS\tZR," + return 0.0 +} + +func Float32ConstantChipFloat() float32 { + // arm64:"FMOVS\t[$]\\(2\\.25\\)," + return 2.25 +} + func Float32Constant() float32 { + // arm64:"FMOVS\t[$]f32\\.42440000\\(SB\\)" // ppc64x/power8:"FMOVS\t[$]f32\\.42440000\\(SB\\)" // ppc64x/power9:"FMOVS\t[$]f32\\.42440000\\(SB\\)" // ppc64x/power10:"XXSPLTIDP\t[$]1111752704," return 49.0 } +func Float64ConstantZero() float64 { + // arm64:"FMOVD\tZR," + return 0.0 +} + +func Float64ConstantChipFloat() float64 { + // arm64:"FMOVD\t[$]\\(2\\.25\\)," + return 2.25 +} + func Float64Constant() float64 { + // arm64:"FMOVD\t[$]f64\\.4048800000000000\\(SB\\)" // ppc64x/power8:"FMOVD\t[$]f64\\.4048800000000000\\(SB\\)" // ppc64x/power9:"FMOVD\t[$]f64\\.4048800000000000\\(SB\\)" // ppc64x/power10:"XXSPLTIDP\t[$]1111752704," @@ -244,11 +266,12 @@ func Float64DenormalFloat32Constant() float64 { return 0x1p-127 } -func Float64ConstantStore(p *float64) { - // amd64: "MOVQ\t[$]4617801906721357038" +func Float32ConstantStore(p *float32) { + // amd64:"MOVL\t[$]1085133554" *p = 5.432 } -func Float32ConstantStore(p *float32) { - // amd64: "MOVL\t[$]1085133554" + +func Float64ConstantStore(p *float64) { + // amd64:"MOVQ\t[$]4617801906721357038" *p = 5.432 } From c9257151e5600af10cdd6c6db907ed83811a54a4 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 24 Sep 2025 08:58:30 +0200 Subject: [PATCH 26/63] runtime: unify ppc64/ppc64le library entry point Cq-Include-Trybots: luci.golang.try:gotip-linux-ppc64le_power10 Change-Id: Ifd7861488b1b47a5d30163552b51838f2bef7248 Reviewed-on: https://go-review.googlesource.com/c/go/+/706395 Reviewed-by: Carlos Amedee Reviewed-by: Keith Randall Reviewed-by: Keith Randall Auto-Submit: Keith Randall LUCI-TryBot-Result: Go LUCI --- src/runtime/asm_ppc64x.s | 57 ++++++++++++ src/runtime/rt0_aix_ppc64.s | 151 +------------------------------- src/runtime/rt0_linux_ppc64le.s | 49 +---------- 3 files changed, 60 insertions(+), 197 deletions(-) diff --git a/src/runtime/asm_ppc64x.s b/src/runtime/asm_ppc64x.s index fc70fa82046056..3fbf11b5e9a2ad 100644 --- a/src/runtime/asm_ppc64x.s +++ b/src/runtime/asm_ppc64x.s @@ -9,6 +9,63 @@ #include "funcdata.h" #include "textflag.h" #include "asm_ppc64x.h" +#include "cgo/abi_ppc64x.h" + + +TEXT _rt0_ppc64x_lib(SB),NOSPLIT|NOFRAME,$0 + // This is called with ELFv2 calling conventions. Convert to Go. + // Allocate argument storage for call to newosproc0. + STACK_AND_SAVE_HOST_TO_GO_ABI(16) + + MOVD R3, _rt0_ppc64x_lib_argc<>(SB) + MOVD R4, _rt0_ppc64x_lib_argv<>(SB) + + // Synchronous initialization. + MOVD $runtime·libpreinit(SB), R12 + MOVD R12, CTR + BL (CTR) + + // Create a new thread to do the runtime initialization and return. + MOVD _cgo_sys_thread_create(SB), R12 + CMP $0, R12 + BEQ nocgo + MOVD $_rt0_ppc64x_lib_go(SB), R3 + MOVD $0, R4 + MOVD R12, CTR + BL (CTR) + BR done + +nocgo: + MOVD $0x800000, R12 // stacksize = 8192KB + MOVD R12, 8+FIXED_FRAME(R1) + MOVD $_rt0_ppc64x_lib_go(SB), R12 + MOVD R12, 16+FIXED_FRAME(R1) + MOVD $runtime·newosproc0(SB),R12 + MOVD R12, CTR + BL (CTR) + +done: + // Restore and return to ELFv2 caller. + UNSTACK_AND_RESTORE_GO_TO_HOST_ABI(16) + RET + +#ifdef GO_PPC64X_HAS_FUNCDESC +DEFINE_PPC64X_FUNCDESC(_rt0_ppc64x_lib_go, __rt0_ppc64x_lib_go) +TEXT __rt0_ppc64x_lib_go(SB),NOSPLIT,$0 +#else +TEXT _rt0_ppc64x_lib_go(SB),NOSPLIT,$0 +#endif + MOVD _rt0_ppc64x_lib_argc<>(SB), R3 + MOVD _rt0_ppc64x_lib_argv<>(SB), R4 + MOVD $runtime·rt0_go(SB), R12 + MOVD R12, CTR + BR (CTR) + +DATA _rt0_ppc64x_lib_argc<>(SB)/8, $0 +GLOBL _rt0_ppc64x_lib_argc<>(SB),NOPTR, $8 +DATA _rt0_ppc64x_lib_argv<>(SB)/8, $0 +GLOBL _rt0_ppc64x_lib_argv<>(SB),NOPTR, $8 + #ifdef GOOS_aix #define cgoCalleeStackSize 48 diff --git a/src/runtime/rt0_aix_ppc64.s b/src/runtime/rt0_aix_ppc64.s index 74c57bb1dc9136..32f8c72156c9ee 100644 --- a/src/runtime/rt0_aix_ppc64.s +++ b/src/runtime/rt0_aix_ppc64.s @@ -41,152 +41,5 @@ TEXT _main(SB),NOSPLIT,$-8 MOVD R12, CTR BR (CTR) -// Paramater save space required to cross-call into _cgo_sys_thread_create -#define PARAM_SPACE 16 - -TEXT _rt0_ppc64_aix_lib(SB),NOSPLIT,$-8 - // Start with standard C stack frame layout and linkage. - MOVD LR, R0 - MOVD R0, 16(R1) // Save LR in caller's frame. - MOVW CR, R0 // Save CR in caller's frame - MOVD R0, 8(R1) - - MOVDU R1, -344-PARAM_SPACE(R1) // Allocate frame. - - // Preserve callee-save registers. - MOVD R14, 48+PARAM_SPACE(R1) - MOVD R15, 56+PARAM_SPACE(R1) - MOVD R16, 64+PARAM_SPACE(R1) - MOVD R17, 72+PARAM_SPACE(R1) - MOVD R18, 80+PARAM_SPACE(R1) - MOVD R19, 88+PARAM_SPACE(R1) - MOVD R20, 96+PARAM_SPACE(R1) - MOVD R21,104+PARAM_SPACE(R1) - MOVD R22, 112+PARAM_SPACE(R1) - MOVD R23, 120+PARAM_SPACE(R1) - MOVD R24, 128+PARAM_SPACE(R1) - MOVD R25, 136+PARAM_SPACE(R1) - MOVD R26, 144+PARAM_SPACE(R1) - MOVD R27, 152+PARAM_SPACE(R1) - MOVD R28, 160+PARAM_SPACE(R1) - MOVD R29, 168+PARAM_SPACE(R1) - MOVD g, 176+PARAM_SPACE(R1) // R30 - MOVD R31, 184+PARAM_SPACE(R1) - FMOVD F14, 192+PARAM_SPACE(R1) - FMOVD F15, 200+PARAM_SPACE(R1) - FMOVD F16, 208+PARAM_SPACE(R1) - FMOVD F17, 216+PARAM_SPACE(R1) - FMOVD F18, 224+PARAM_SPACE(R1) - FMOVD F19, 232+PARAM_SPACE(R1) - FMOVD F20, 240+PARAM_SPACE(R1) - FMOVD F21, 248+PARAM_SPACE(R1) - FMOVD F22, 256+PARAM_SPACE(R1) - FMOVD F23, 264+PARAM_SPACE(R1) - FMOVD F24, 272+PARAM_SPACE(R1) - FMOVD F25, 280+PARAM_SPACE(R1) - FMOVD F26, 288+PARAM_SPACE(R1) - FMOVD F27, 296+PARAM_SPACE(R1) - FMOVD F28, 304+PARAM_SPACE(R1) - FMOVD F29, 312+PARAM_SPACE(R1) - FMOVD F30, 320+PARAM_SPACE(R1) - FMOVD F31, 328+PARAM_SPACE(R1) - - // Synchronous initialization. - MOVD $runtime·reginit(SB), R12 - MOVD R12, CTR - BL (CTR) - - MOVBZ runtime·isarchive(SB), R3 // Check buildmode = c-archive - CMP $0, R3 - BEQ done - - MOVD R14, _rt0_ppc64_aix_lib_argc<>(SB) - MOVD R15, _rt0_ppc64_aix_lib_argv<>(SB) - - MOVD $runtime·libpreinit(SB), R12 - MOVD R12, CTR - BL (CTR) - - // Create a new thread to do the runtime initialization and return. - MOVD _cgo_sys_thread_create(SB), R12 - CMP $0, R12 - BEQ nocgo - MOVD $_rt0_ppc64_aix_lib_go(SB), R3 - MOVD $0, R4 - MOVD R2, 40(R1) - MOVD 8(R12), R2 - MOVD (R12), R12 - MOVD R12, CTR - BL (CTR) - MOVD 40(R1), R2 - BR done - -nocgo: - MOVD $0x800000, R12 // stacksize = 8192KB - MOVD R12, 8(R1) - MOVD $_rt0_ppc64_aix_lib_go(SB), R12 - MOVD R12, 16(R1) - MOVD $runtime·newosproc0(SB),R12 - MOVD R12, CTR - BL (CTR) - -done: - // Restore saved registers. - MOVD 48+PARAM_SPACE(R1), R14 - MOVD 56+PARAM_SPACE(R1), R15 - MOVD 64+PARAM_SPACE(R1), R16 - MOVD 72+PARAM_SPACE(R1), R17 - MOVD 80+PARAM_SPACE(R1), R18 - MOVD 88+PARAM_SPACE(R1), R19 - MOVD 96+PARAM_SPACE(R1), R20 - MOVD 104+PARAM_SPACE(R1), R21 - MOVD 112+PARAM_SPACE(R1), R22 - MOVD 120+PARAM_SPACE(R1), R23 - MOVD 128+PARAM_SPACE(R1), R24 - MOVD 136+PARAM_SPACE(R1), R25 - MOVD 144+PARAM_SPACE(R1), R26 - MOVD 152+PARAM_SPACE(R1), R27 - MOVD 160+PARAM_SPACE(R1), R28 - MOVD 168+PARAM_SPACE(R1), R29 - MOVD 176+PARAM_SPACE(R1), g // R30 - MOVD 184+PARAM_SPACE(R1), R31 - FMOVD 196+PARAM_SPACE(R1), F14 - FMOVD 200+PARAM_SPACE(R1), F15 - FMOVD 208+PARAM_SPACE(R1), F16 - FMOVD 216+PARAM_SPACE(R1), F17 - FMOVD 224+PARAM_SPACE(R1), F18 - FMOVD 232+PARAM_SPACE(R1), F19 - FMOVD 240+PARAM_SPACE(R1), F20 - FMOVD 248+PARAM_SPACE(R1), F21 - FMOVD 256+PARAM_SPACE(R1), F22 - FMOVD 264+PARAM_SPACE(R1), F23 - FMOVD 272+PARAM_SPACE(R1), F24 - FMOVD 280+PARAM_SPACE(R1), F25 - FMOVD 288+PARAM_SPACE(R1), F26 - FMOVD 296+PARAM_SPACE(R1), F27 - FMOVD 304+PARAM_SPACE(R1), F28 - FMOVD 312+PARAM_SPACE(R1), F29 - FMOVD 320+PARAM_SPACE(R1), F30 - FMOVD 328+PARAM_SPACE(R1), F31 - - ADD $344+PARAM_SPACE, R1 - - MOVD 8(R1), R0 - MOVFL R0, $0xff - MOVD 16(R1), R0 - MOVD R0, LR - RET - -DEFINE_PPC64X_FUNCDESC(_rt0_ppc64_aix_lib_go, __rt0_ppc64_aix_lib_go) - -TEXT __rt0_ppc64_aix_lib_go(SB),NOSPLIT,$0 - MOVD _rt0_ppc64_aix_lib_argc<>(SB), R3 - MOVD _rt0_ppc64_aix_lib_argv<>(SB), R4 - MOVD $runtime·rt0_go(SB), R12 - MOVD R12, CTR - BR (CTR) - -DATA _rt0_ppc64_aix_lib_argc<>(SB)/8, $0 -GLOBL _rt0_ppc64_aix_lib_argc<>(SB),NOPTR, $8 -DATA _rt0_ppc64_aix_lib_argv<>(SB)/8, $0 -GLOBL _rt0_ppc64_aix_lib_argv<>(SB),NOPTR, $8 +TEXT _rt0_ppc64_aix_lib(SB),NOSPLIT,$0 + JMP _rt0_ppc64x_lib(SB) diff --git a/src/runtime/rt0_linux_ppc64le.s b/src/runtime/rt0_linux_ppc64le.s index 4b7d8e1b940a1e..3a6e8863b2da1d 100644 --- a/src/runtime/rt0_linux_ppc64le.s +++ b/src/runtime/rt0_linux_ppc64le.s @@ -5,60 +5,13 @@ #include "go_asm.h" #include "textflag.h" #include "asm_ppc64x.h" -#include "cgo/abi_ppc64x.h" TEXT _rt0_ppc64le_linux(SB),NOSPLIT,$0 XOR R0, R0 // Make sure R0 is zero before _main BR _main<>(SB) TEXT _rt0_ppc64le_linux_lib(SB),NOSPLIT|NOFRAME,$0 - // This is called with ELFv2 calling conventions. Convert to Go. - // Allocate argument storage for call to newosproc0. - STACK_AND_SAVE_HOST_TO_GO_ABI(16) - - MOVD R3, _rt0_ppc64le_linux_lib_argc<>(SB) - MOVD R4, _rt0_ppc64le_linux_lib_argv<>(SB) - - // Synchronous initialization. - MOVD $runtime·libpreinit(SB), R12 - MOVD R12, CTR - BL (CTR) - - // Create a new thread to do the runtime initialization and return. - MOVD _cgo_sys_thread_create(SB), R12 - CMP $0, R12 - BEQ nocgo - MOVD $_rt0_ppc64le_linux_lib_go(SB), R3 - MOVD $0, R4 - MOVD R12, CTR - BL (CTR) - BR done - -nocgo: - MOVD $0x800000, R12 // stacksize = 8192KB - MOVD R12, 8+FIXED_FRAME(R1) - MOVD $_rt0_ppc64le_linux_lib_go(SB), R12 - MOVD R12, 16+FIXED_FRAME(R1) - MOVD $runtime·newosproc0(SB),R12 - MOVD R12, CTR - BL (CTR) - -done: - // Restore and return to ELFv2 caller. - UNSTACK_AND_RESTORE_GO_TO_HOST_ABI(16) - RET - -TEXT _rt0_ppc64le_linux_lib_go(SB),NOSPLIT,$0 - MOVD _rt0_ppc64le_linux_lib_argc<>(SB), R3 - MOVD _rt0_ppc64le_linux_lib_argv<>(SB), R4 - MOVD $runtime·rt0_go(SB), R12 - MOVD R12, CTR - BR (CTR) - -DATA _rt0_ppc64le_linux_lib_argc<>(SB)/8, $0 -GLOBL _rt0_ppc64le_linux_lib_argc<>(SB),NOPTR, $8 -DATA _rt0_ppc64le_linux_lib_argv<>(SB)/8, $0 -GLOBL _rt0_ppc64le_linux_lib_argv<>(SB),NOPTR, $8 + JMP _rt0_ppc64x_lib(SB) TEXT _main<>(SB),NOSPLIT,$-8 // In a statically linked binary, the stack contains argc, From eb1c7f6e69b0e62067ff22a0656cedff792c8438 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 24 Sep 2025 10:34:25 +0200 Subject: [PATCH 27/63] runtime: move loong64 library entry point to os-agnostic file The library entry point for loong64 is agnostic to the OS, so move it to asm_loong64.s. This is similar to what we do for other architectures. Cq-Include-Trybots: luci.golang.try:gotip-linux-loong64 Change-Id: I6915eb76d3ea72a779e05e78d85f24793169c61f Reviewed-on: https://go-review.googlesource.com/c/go/+/706416 Reviewed-by: abner chenc Reviewed-by: Cherry Mui Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI --- src/runtime/asm_loong64.s | 51 +++++++++++++++++++++++++++++++++ src/runtime/rt0_linux_loong64.s | 50 ++------------------------------ 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/runtime/asm_loong64.s b/src/runtime/asm_loong64.s index ee7f825e1f6681..586bf89a5d3729 100644 --- a/src/runtime/asm_loong64.s +++ b/src/runtime/asm_loong64.s @@ -6,6 +6,57 @@ #include "go_tls.h" #include "funcdata.h" #include "textflag.h" +#include "cgo/abi_loong64.h" + +// When building with -buildmode=c-shared, this symbol is called when the shared +// library is loaded. +TEXT _rt0_loong64_lib(SB),NOSPLIT,$168 + // Preserve callee-save registers. + SAVE_R22_TO_R31(3*8) + SAVE_F24_TO_F31(13*8) + + // Initialize g as nil in case of using g later e.g. sigaction in cgo_sigaction.go + MOVV R0, g + + MOVV R4, _rt0_loong64_lib_argc<>(SB) + MOVV R5, _rt0_loong64_lib_argv<>(SB) + + // Synchronous initialization. + MOVV $runtime·libpreinit(SB), R19 + JAL (R19) + + // Create a new thread to do the runtime initialization and return. + MOVV _cgo_sys_thread_create(SB), R19 + BEQ R19, nocgo + MOVV $_rt0_loong64_lib_go(SB), R4 + MOVV $0, R5 + JAL (R19) + JMP restore + +nocgo: + MOVV $0x800000, R4 // stacksize = 8192KB + MOVV $_rt0_loong64_lib_go(SB), R5 + MOVV R4, 8(R3) + MOVV R5, 16(R3) + MOVV $runtime·newosproc0(SB), R19 + JAL (R19) + +restore: + // Restore callee-save registers. + RESTORE_R22_TO_R31(3*8) + RESTORE_F24_TO_F31(13*8) + RET + +TEXT _rt0_loong64_lib_go(SB),NOSPLIT,$0 + MOVV _rt0_loong64_lib_argc<>(SB), R4 + MOVV _rt0_loong64_lib_argv<>(SB), R5 + MOVV $runtime·rt0_go(SB),R19 + JMP (R19) + +DATA _rt0_loong64_lib_argc<>(SB)/8, $0 +GLOBL _rt0_loong64_lib_argc<>(SB),NOPTR, $8 +DATA _rt0_loong64_lib_argv<>(SB)/8, $0 +GLOBL _rt0_loong64_lib_argv<>(SB),NOPTR, $8 #define REGCTXT R29 diff --git a/src/runtime/rt0_linux_loong64.s b/src/runtime/rt0_linux_loong64.s index b52f7d530a6a98..d8da4461a2f56d 100644 --- a/src/runtime/rt0_linux_loong64.s +++ b/src/runtime/rt0_linux_loong64.s @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. #include "textflag.h" -#include "cgo/abi_loong64.h" TEXT _rt0_loong64_linux(SB),NOSPLIT|NOFRAME,$0 // In a statically linked binary, the stack contains argc, @@ -16,53 +15,8 @@ TEXT _rt0_loong64_linux(SB),NOSPLIT|NOFRAME,$0 // When building with -buildmode=c-shared, this symbol is called when the shared // library is loaded. -TEXT _rt0_loong64_linux_lib(SB),NOSPLIT,$168 - // Preserve callee-save registers. - SAVE_R22_TO_R31(3*8) - SAVE_F24_TO_F31(13*8) - - // Initialize g as nil in case of using g later e.g. sigaction in cgo_sigaction.go - MOVV R0, g - - MOVV R4, _rt0_loong64_linux_lib_argc<>(SB) - MOVV R5, _rt0_loong64_linux_lib_argv<>(SB) - - // Synchronous initialization. - MOVV $runtime·libpreinit(SB), R19 - JAL (R19) - - // Create a new thread to do the runtime initialization and return. - MOVV _cgo_sys_thread_create(SB), R19 - BEQ R19, nocgo - MOVV $_rt0_loong64_linux_lib_go(SB), R4 - MOVV $0, R5 - JAL (R19) - JMP restore - -nocgo: - MOVV $0x800000, R4 // stacksize = 8192KB - MOVV $_rt0_loong64_linux_lib_go(SB), R5 - MOVV R4, 8(R3) - MOVV R5, 16(R3) - MOVV $runtime·newosproc0(SB), R19 - JAL (R19) - -restore: - // Restore callee-save registers. - RESTORE_R22_TO_R31(3*8) - RESTORE_F24_TO_F31(13*8) - RET - -TEXT _rt0_loong64_linux_lib_go(SB),NOSPLIT,$0 - MOVV _rt0_loong64_linux_lib_argc<>(SB), R4 - MOVV _rt0_loong64_linux_lib_argv<>(SB), R5 - MOVV $runtime·rt0_go(SB),R19 - JMP (R19) - -DATA _rt0_loong64_linux_lib_argc<>(SB)/8, $0 -GLOBL _rt0_loong64_linux_lib_argc<>(SB),NOPTR, $8 -DATA _rt0_loong64_linux_lib_argv<>(SB)/8, $0 -GLOBL _rt0_loong64_linux_lib_argv<>(SB),NOPTR, $8 +TEXT _rt0_loong64_linux_lib(SB),NOSPLIT,$0 + JMP _rt0_loong64_lib(SB) TEXT main(SB),NOSPLIT|NOFRAME,$0 // in external linking, glibc jumps to main with argc in R4 From be0fed8a5fc4e34f2c6caf503830bcdf904ded54 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 30 Sep 2025 15:11:58 -0400 Subject: [PATCH 28/63] cmd/go/testdata/script/test_fuzz_fuzztime.txt: disable This test features a 5s timeout, which is far too close to the natural variance in scheduling on an overloaded CI builder machine to make a reliable test. Skipping. Updates #72104 Change-Id: I52133a2d101808c923e316e0c7fdce9edbb31b10 Reviewed-on: https://go-review.googlesource.com/c/go/+/708075 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob Reviewed-by: Michael Matloob --- src/cmd/go/testdata/script/test_fuzz_fuzztime.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cmd/go/testdata/script/test_fuzz_fuzztime.txt b/src/cmd/go/testdata/script/test_fuzz_fuzztime.txt index 027c434a322ec1..3cc23985a3934a 100644 --- a/src/cmd/go/testdata/script/test_fuzz_fuzztime.txt +++ b/src/cmd/go/testdata/script/test_fuzz_fuzztime.txt @@ -1,3 +1,5 @@ +skip # a 5s timeout is never going to be reliable (go.dev/issue/72140) + [!fuzz] skip [short] skip env GOCACHE=$WORK/cache From 3f451f2c54c87db8b8f30e4d5224933f7895f453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20G=2E=20MARAND?= Date: Wed, 1 Oct 2025 12:46:16 +0000 Subject: [PATCH 29/63] testing/synctest: fix inverted test failure message in TestContextAfterFunc Fixes #75685 Change-Id: I5592becfde6aaca3d7f0e2f09bc7a9785228523e GitHub-Last-Rev: 0ff7bd31ecfc23222dae70194621397330f3c2da GitHub-Pull-Request: golang/go#75687 Reviewed-on: https://go-review.googlesource.com/c/go/+/708275 Reviewed-by: Alan Donovan Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil Auto-Submit: Sean Liao Reviewed-by: Damien Neil Reviewed-by: Sean Liao --- src/testing/synctest/example_test.go | 2 +- src/testing/synctest/synctest.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testing/synctest/example_test.go b/src/testing/synctest/example_test.go index 843377ea88ef71..a86d87fcecdca1 100644 --- a/src/testing/synctest/example_test.go +++ b/src/testing/synctest/example_test.go @@ -66,7 +66,7 @@ func TestContextAfterFunc(t *testing.T) { cancel() synctest.Wait() if !afterFuncCalled { - t.Fatalf("before context is canceled: AfterFunc not called") + t.Fatalf("after context is canceled: AfterFunc not called") } }) } diff --git a/src/testing/synctest/synctest.go b/src/testing/synctest/synctest.go index 707383f9c75499..9f499515b8881a 100644 --- a/src/testing/synctest/synctest.go +++ b/src/testing/synctest/synctest.go @@ -147,7 +147,7 @@ // cancel() // synctest.Wait() // if !afterFuncCalled { -// t.Fatalf("before context is canceled: AfterFunc not called") +// t.Fatalf("after context is canceled: AfterFunc not called") // } // }) // } From 8ad27fb656ab162546137b512c61f6a26a90a6c5 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 1 Oct 2025 15:07:57 -0400 Subject: [PATCH 30/63] doc/go_spec.html: update date (addressing comment from review of CL 704737) Change-Id: I483dea046f664035e79c51729203c9a9ff0cbc59 Reviewed-on: https://go-review.googlesource.com/c/go/+/708299 Auto-Submit: Alan Donovan Reviewed-by: Robert Griesemer LUCI-TryBot-Result: Go LUCI --- doc/go_spec.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/go_spec.html b/doc/go_spec.html index b67eaf9999ab1e..92afe1cee0baef 100644 --- a/doc/go_spec.html +++ b/doc/go_spec.html @@ -1,6 +1,6 @@ From 633dd1d475e7346b43d87abc987a8c7f256e827d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 15 Sep 2025 12:56:11 -0700 Subject: [PATCH 31/63] encoding/json: fix Decoder.InputOffset regression in goexperiment.jsonv2 The Decoder.InputOffset method was always ambiguous about the exact offset returned since anything between the end of the previous token to the start of the next token could be a valid result. Empirically, it seems that the behavior was to report the end of the previous token unless Decoder.More is called, in which case it reports the start of the next token. This is an odd semantic since a relatively side-effect free method like More is not quite so side-effect free. However, our goal is to preserve historical v1 semantic when possible regardless of whether it made sense. Note that jsontext.Decoder.InputOffset consistently always reports the end of the previous token. Users can explicitly choose the exact position they want by inspecting the UnreadBuffer. Fixes #75468 Change-Id: I1e946e83c9d29dfc09f2913ff8d6b2b80632f292 Reviewed-on: https://go-review.googlesource.com/c/go/+/703856 Reviewed-by: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao --- src/encoding/json/stream_test.go | 64 +++++++++++++++++++++++++++++ src/encoding/json/v2_stream.go | 22 +++++++++- src/encoding/json/v2_stream_test.go | 64 +++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/src/encoding/json/stream_test.go b/src/encoding/json/stream_test.go index 9e5d48d39d2fcf..0e937cfaa13c78 100644 --- a/src/encoding/json/stream_test.go +++ b/src/encoding/json/stream_test.go @@ -557,3 +557,67 @@ func TestTokenTruncation(t *testing.T) { } } } + +func TestDecoderInputOffset(t *testing.T) { + const input = ` [ + [ ] , [ "one" ] , [ "one" , "two" ] , + { } , { "alpha" : "bravo" } , { "alpha" : "bravo" , "fizz" : "buzz" } + ] ` + wantOffsets := []int64{ + 0, 1, 2, 5, 6, 7, 8, 9, 12, 13, 18, 19, 20, 21, 24, 25, 30, 31, + 38, 39, 40, 41, 46, 47, 48, 49, 52, 53, 60, 61, 70, 71, 72, 73, + 76, 77, 84, 85, 94, 95, 103, 104, 112, 113, 114, 116, 117, 117, + 117, 117, + } + wantMores := []bool{ + true, true, false, true, true, false, true, true, true, false, + true, false, true, true, true, false, true, true, true, true, + true, false, false, false, false, + } + + d := NewDecoder(strings.NewReader(input)) + checkOffset := func() { + t.Helper() + got := d.InputOffset() + if len(wantOffsets) == 0 { + t.Fatalf("InputOffset = %d, want nil", got) + } + want := wantOffsets[0] + if got != want { + t.Fatalf("InputOffset = %d, want %d", got, want) + } + wantOffsets = wantOffsets[1:] + } + checkMore := func() { + t.Helper() + got := d.More() + if len(wantMores) == 0 { + t.Fatalf("More = %v, want nil", got) + } + want := wantMores[0] + if got != want { + t.Fatalf("More = %v, want %v", got, want) + } + wantMores = wantMores[1:] + } + checkOffset() + checkMore() + checkOffset() + for { + if _, err := d.Token(); err == io.EOF { + break + } else if err != nil { + t.Fatalf("Token error: %v", err) + } + checkOffset() + checkMore() + checkOffset() + } + checkOffset() + checkMore() + checkOffset() + + if len(wantOffsets)+len(wantMores) > 0 { + t.Fatal("unconsumed testdata") + } +} diff --git a/src/encoding/json/v2_stream.go b/src/encoding/json/v2_stream.go index 28e72c0a529e15..dcee553ee13336 100644 --- a/src/encoding/json/v2_stream.go +++ b/src/encoding/json/v2_stream.go @@ -20,6 +20,10 @@ type Decoder struct { dec *jsontext.Decoder opts jsonv2.Options err error + + // hadPeeked reports whether [Decoder.More] was called. + // It is reset by [Decoder.Decode] and [Decoder.Token]. + hadPeeked bool } // NewDecoder returns a new decoder that reads from r. @@ -76,6 +80,7 @@ func (dec *Decoder) Decode(v any) error { } return dec.err } + dec.hadPeeked = false return jsonv2.Unmarshal(b, v, dec.opts) } @@ -206,6 +211,7 @@ func (dec *Decoder) Token() (Token, error) { } return nil, transformSyntacticError(err) } + dec.hadPeeked = false switch k := tok.Kind(); k { case 'n': return nil, nil @@ -230,6 +236,7 @@ func (dec *Decoder) Token() (Token, error) { // More reports whether there is another element in the // current array or object being parsed. func (dec *Decoder) More() bool { + dec.hadPeeked = true k := dec.dec.PeekKind() return k > 0 && k != ']' && k != '}' } @@ -238,5 +245,18 @@ func (dec *Decoder) More() bool { // The offset gives the location of the end of the most recently returned token // and the beginning of the next token. func (dec *Decoder) InputOffset() int64 { - return dec.dec.InputOffset() + offset := dec.dec.InputOffset() + if dec.hadPeeked { + // Historically, InputOffset reported the location of + // the end of the most recently returned token + // unless [Decoder.More] is called, in which case, it reported + // the beginning of the next token. + unreadBuffer := dec.dec.UnreadBuffer() + trailingTokens := bytes.TrimLeft(unreadBuffer, " \n\r\t") + if len(trailingTokens) > 0 { + leadingWhitespace := len(unreadBuffer) - len(trailingTokens) + offset += int64(leadingWhitespace) + } + } + return offset } diff --git a/src/encoding/json/v2_stream_test.go b/src/encoding/json/v2_stream_test.go index b8951f82054e96..0885631fb5937f 100644 --- a/src/encoding/json/v2_stream_test.go +++ b/src/encoding/json/v2_stream_test.go @@ -537,3 +537,67 @@ func TestTokenTruncation(t *testing.T) { } } } + +func TestDecoderInputOffset(t *testing.T) { + const input = ` [ + [ ] , [ "one" ] , [ "one" , "two" ] , + { } , { "alpha" : "bravo" } , { "alpha" : "bravo" , "fizz" : "buzz" } + ] ` + wantOffsets := []int64{ + 0, 1, 2, 5, 6, 7, 8, 9, 12, 13, 18, 19, 20, 21, 24, 25, 30, 31, + 38, 39, 40, 41, 46, 47, 48, 49, 52, 53, 60, 61, 70, 71, 72, 73, + 76, 77, 84, 85, 94, 95, 103, 104, 112, 113, 114, 116, 117, 117, + 117, 117, + } + wantMores := []bool{ + true, true, false, true, true, false, true, true, true, false, + true, false, true, true, true, false, true, true, true, true, + true, false, false, false, false, + } + + d := NewDecoder(strings.NewReader(input)) + checkOffset := func() { + t.Helper() + got := d.InputOffset() + if len(wantOffsets) == 0 { + t.Fatalf("InputOffset = %d, want nil", got) + } + want := wantOffsets[0] + if got != want { + t.Fatalf("InputOffset = %d, want %d", got, want) + } + wantOffsets = wantOffsets[1:] + } + checkMore := func() { + t.Helper() + got := d.More() + if len(wantMores) == 0 { + t.Fatalf("More = %v, want nil", got) + } + want := wantMores[0] + if got != want { + t.Fatalf("More = %v, want %v", got, want) + } + wantMores = wantMores[1:] + } + checkOffset() + checkMore() + checkOffset() + for { + if _, err := d.Token(); err == io.EOF { + break + } else if err != nil { + t.Fatalf("Token error: %v", err) + } + checkOffset() + checkMore() + checkOffset() + } + checkOffset() + checkMore() + checkOffset() + + if len(wantOffsets)+len(wantMores) > 0 { + t.Fatal("unconsumed testdata") + } +} From 5799c139a77e9c3a5750c90ebda538131f4517d6 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Mon, 29 Sep 2025 14:31:38 -0400 Subject: [PATCH 32/63] crypto/tls: rm marshalEncryptedClientHelloConfigList dead code This package internal function has no call sites. Change-Id: I262058199fd2f387ef3b5e21099421720cc5413e Reviewed-on: https://go-review.googlesource.com/c/go/+/707815 TryBot-Bypass: Daniel McCarney Auto-Submit: Roland Shoemaker Auto-Submit: Daniel McCarney Reviewed-by: Carlos Amedee Reviewed-by: Roland Shoemaker --- src/crypto/tls/ech.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/crypto/tls/ech.go b/src/crypto/tls/ech.go index 76727a890896a0..d3472d8dc4aafb 100644 --- a/src/crypto/tls/ech.go +++ b/src/crypto/tls/ech.go @@ -568,16 +568,6 @@ func parseECHExt(ext []byte) (echType echExtType, cs echCipher, configID uint8, return echType, cs, configID, bytes.Clone(encap), bytes.Clone(payload), nil } -func marshalEncryptedClientHelloConfigList(configs []EncryptedClientHelloKey) ([]byte, error) { - builder := cryptobyte.NewBuilder(nil) - builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) { - for _, c := range configs { - builder.AddBytes(c.Config) - } - }) - return builder.Bytes() -} - func (c *Conn) processECHClientHello(outer *clientHelloMsg, echKeys []EncryptedClientHelloKey) (*clientHelloMsg, *echServerContext, error) { echType, echCiphersuite, configID, encap, payload, err := parseECHExt(outer.encryptedClientHello) if err != nil { From 84db201ae18c889acdefe20c8a903b188328f16d Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Fri, 26 Sep 2025 20:47:45 +0200 Subject: [PATCH 33/63] cmd/compile: propagate len([]T{}) to make builtin to allow stack allocation Updates #75620 Change-Id: I6a6a6964af4512e30eb4806e1dc7b0fd0835744f Reviewed-on: https://go-review.googlesource.com/c/go/+/707255 Reviewed-by: Keith Randall Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI Auto-Submit: Keith Randall Reviewed-by: Carlos Amedee --- src/cmd/compile/internal/escape/escape.go | 13 ++++++++++++- test/escape_make_non_const.go | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/cmd/compile/internal/escape/escape.go b/src/cmd/compile/internal/escape/escape.go index 6b34830b3dd5e6..59250edfef0078 100644 --- a/src/cmd/compile/internal/escape/escape.go +++ b/src/cmd/compile/internal/escape/escape.go @@ -563,7 +563,10 @@ func (b *batch) rewriteWithLiterals(n ir.Node, fn *ir.Func) { if ro == nil { base.Fatalf("no ReassignOracle for function %v with closure parent %v", fn, fn.ClosureParent) } - if s := ro.StaticValue(*r); s.Op() == ir.OLITERAL { + + s := ro.StaticValue(*r) + switch s.Op() { + case ir.OLITERAL: lit, ok := s.(*ir.BasicLit) if !ok || lit.Val().Kind() != constant.Int { base.Fatalf("unexpected BasicLit Kind") @@ -577,6 +580,14 @@ func (b *batch) rewriteWithLiterals(n ir.Node, fn *ir.Func) { assignTemp(n.Pos(), *r, n.PtrInit()) *r = ir.NewBasicLit(n.Pos(), (*r).Type(), lit.Val()) } + case ir.OLEN: + x := ro.StaticValue(s.(*ir.UnaryExpr).X) + if x.Op() == ir.OSLICELIT { + x := x.(*ir.CompLitExpr) + // Preserve any side effects of the original expression, then update the value. + assignTemp(n.Pos(), *r, n.PtrInit()) + *r = ir.NewBasicLit(n.Pos(), types.Types[types.TINT], constant.MakeInt64(x.Len)) + } } } case ir.OCONVIFACE: diff --git a/test/escape_make_non_const.go b/test/escape_make_non_const.go index 7a9b28d5e37a7e..11854ac4f4731a 100644 --- a/test/escape_make_non_const.go +++ b/test/escape_make_non_const.go @@ -106,3 +106,18 @@ type m struct { func newM(l int) m { // ERROR "can inline" return m{make(map[string]int, l)} // ERROR "make.*escapes to heap" } + +//go:noinline +func testLenOfSliceLit() { + ints := []int{0, 1, 2, 3, 4, 5} // ERROR "\[\]int\{\.\.\.\} does not escape"' + _ = make([]int, len(ints)) // ERROR "make\(\[\]int, 6\) does not escape" + _ = allocLenOf(ints) // ERROR "inlining call", "make\(\[\]int, 6\) does not escape" + + _ = make([]int, 2, len(ints)) // ERROR "make\(\[\]int, 2, 6\) does not escape" + _ = make([]int, len(ints), 2) // ERROR "make\(\[\]int, len\(ints\), 2\) does not escape" + _ = make([]int, 10, len(ints)) // ERROR "make\(\[\]int, 10, 6\) does not escape" +} + +func allocLenOf(s []int) []int { // ERROR "can inline" "s does not escape" + return make([]int, len(s)) // ERROR "escapes to heap" +} From 8c68a1c1abade565c6719159858e76f9b122ddc8 Mon Sep 17 00:00:00 2001 From: Vlad Saioc Date: Thu, 2 Oct 2025 11:57:58 +0000 Subject: [PATCH 34/63] runtime,net/http/pprof: goroutine leak detection by using the garbage collector Proposal #74609 Change-Id: I97a754b128aac1bc5b7b9ab607fcd5bb390058c8 GitHub-Last-Rev: 60f2a192badf415112246de8bc6c0084085314f6 GitHub-Pull-Request: golang/go#74622 Reviewed-on: https://go-review.googlesource.com/c/go/+/688335 LUCI-TryBot-Result: Go LUCI Reviewed-by: t hepudds Auto-Submit: Michael Knyszek Reviewed-by: Michael Knyszek Reviewed-by: Carlos Amedee --- src/cmd/link/internal/loader/loader.go | 3 + .../exp_goroutineleakprofile_off.go | 8 + .../exp_goroutineleakprofile_on.go | 8 + src/internal/goexperiment/flags.go | 3 + src/net/http/pprof/pprof.go | 8 + src/runtime/chan.go | 32 +- src/runtime/crash_test.go | 16 + src/runtime/goroutineleakprofile_test.go | 595 ++++++ src/runtime/mbitmap.go | 22 + src/runtime/mgc.go | 306 ++- src/runtime/mgcmark.go | 105 +- src/runtime/mgcmark_greenteagc.go | 2 +- src/runtime/mgcmark_nogreenteagc.go | 2 +- src/runtime/mprof.go | 56 + src/runtime/pprof/pprof.go | 45 +- src/runtime/pprof/runtime.go | 6 + src/runtime/preempt.go | 3 +- src/runtime/proc.go | 16 +- src/runtime/runtime2.go | 121 +- src/runtime/select.go | 12 +- src/runtime/sema.go | 27 +- src/runtime/sizeof_test.go | 2 +- src/runtime/stack.go | 17 +- .../commonpatterns.go | 277 +++ .../testgoroutineleakprofile/goker/LICENSE | 21 + .../testgoroutineleakprofile/goker/README.md | 1847 +++++++++++++++++ .../goker/cockroach10214.go | 145 ++ .../goker/cockroach1055.go | 115 + .../goker/cockroach10790.go | 98 + .../goker/cockroach13197.go | 82 + .../goker/cockroach13755.go | 66 + .../goker/cockroach1462.go | 167 ++ .../goker/cockroach16167.go | 108 + .../goker/cockroach18101.go | 60 + .../goker/cockroach2448.go | 125 ++ .../goker/cockroach24808.go | 78 + .../goker/cockroach25456.go | 92 + .../goker/cockroach35073.go | 124 ++ .../goker/cockroach35931.go | 135 ++ .../goker/cockroach3710.go | 122 ++ .../goker/cockroach584.go | 62 + .../goker/cockroach6181.go | 87 + .../goker/cockroach7504.go | 183 ++ .../goker/cockroach9935.go | 77 + .../goker/etcd10492.go | 72 + .../goker/etcd5509.go | 126 ++ .../goker/etcd6708.go | 100 + .../goker/etcd6857.go | 81 + .../goker/etcd6873.go | 98 + .../goker/etcd7492.go | 163 ++ .../goker/etcd7902.go | 87 + .../goker/grpc1275.go | 111 + .../goker/grpc1424.go | 105 + .../goker/grpc1460.go | 84 + .../goker/grpc3017.go | 123 ++ .../testgoroutineleakprofile/goker/grpc660.go | 65 + .../testgoroutineleakprofile/goker/grpc795.go | 74 + .../testgoroutineleakprofile/goker/grpc862.go | 105 + .../goker/hugo3251.go | 81 + .../goker/hugo5379.go | 317 +++ .../goker/istio16224.go | 129 ++ .../goker/istio17860.go | 144 ++ .../goker/istio18454.go | 154 ++ .../goker/kubernetes10182.go | 95 + .../goker/kubernetes11298.go | 118 ++ .../goker/kubernetes13135.go | 166 ++ .../goker/kubernetes1321.go | 100 + .../goker/kubernetes25331.go | 72 + .../goker/kubernetes26980.go | 87 + .../goker/kubernetes30872.go | 223 ++ .../goker/kubernetes38669.go | 83 + .../goker/kubernetes5316.go | 68 + .../goker/kubernetes58107.go | 108 + .../goker/kubernetes62464.go | 120 ++ .../goker/kubernetes6632.go | 86 + .../goker/kubernetes70277.go | 97 + .../testgoroutineleakprofile/goker/main.go | 39 + .../goker/moby17176.go | 68 + .../goker/moby21233.go | 146 ++ .../goker/moby25384.go | 59 + .../goker/moby27782.go | 242 +++ .../goker/moby28462.go | 125 ++ .../goker/moby30408.go | 67 + .../goker/moby33781.go | 71 + .../goker/moby36114.go | 53 + .../goker/moby4951.go | 101 + .../goker/moby7559.go | 55 + .../goker/serving2137.go | 122 ++ .../goker/syncthing4829.go | 87 + .../goker/syncthing5795.go | 103 + .../testdata/testgoroutineleakprofile/main.go | 39 + .../testgoroutineleakprofile/simple.go | 253 +++ .../testgoroutineleakprofile/stresstests.go | 89 + src/runtime/traceback.go | 6 +- src/runtime/tracestatus.go | 2 +- 95 files changed, 10766 insertions(+), 89 deletions(-) create mode 100644 src/internal/goexperiment/exp_goroutineleakprofile_off.go create mode 100644 src/internal/goexperiment/exp_goroutineleakprofile_on.go create mode 100644 src/runtime/goroutineleakprofile_test.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/commonpatterns.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/LICENSE create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/README.md create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10214.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1055.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10790.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13197.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13755.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1462.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach16167.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach18101.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach2448.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach24808.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach25456.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35073.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35931.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach3710.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach584.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach6181.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach7504.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/cockroach9935.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd10492.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd5509.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd6708.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd6857.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd6873.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd7492.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/etcd7902.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc1275.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc1424.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc1460.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc3017.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc660.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc795.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/grpc862.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/hugo3251.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/hugo5379.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/istio16224.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/istio17860.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/istio18454.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes10182.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes11298.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes13135.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes1321.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes25331.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes26980.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes30872.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes38669.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes5316.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes58107.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes62464.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes6632.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes70277.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/main.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby17176.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby21233.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby25384.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby27782.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby28462.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby30408.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby33781.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby36114.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby4951.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/moby7559.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/serving2137.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/syncthing4829.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/goker/syncthing5795.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/main.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/simple.go create mode 100644 src/runtime/testdata/testgoroutineleakprofile/stresstests.go diff --git a/src/cmd/link/internal/loader/loader.go b/src/cmd/link/internal/loader/loader.go index 103dad03001263..0ed20d1becbb03 100644 --- a/src/cmd/link/internal/loader/loader.go +++ b/src/cmd/link/internal/loader/loader.go @@ -2450,6 +2450,9 @@ var blockedLinknames = map[string][]string{ "sync_test.runtime_blockUntilEmptyCleanupQueue": {"sync_test"}, "time.runtimeIsBubbled": {"time"}, "unique.runtime_blockUntilEmptyCleanupQueue": {"unique"}, + // Experimental features + "runtime.goroutineLeakGC": {"runtime/pprof"}, + "runtime.goroutineleakcount": {"runtime/pprof"}, // Others "net.newWindowsFile": {"net"}, // pushed from os "testing/synctest.testingSynctestTest": {"testing/synctest"}, // pushed from testing diff --git a/src/internal/goexperiment/exp_goroutineleakprofile_off.go b/src/internal/goexperiment/exp_goroutineleakprofile_off.go new file mode 100644 index 00000000000000..63eafe9e6c74db --- /dev/null +++ b/src/internal/goexperiment/exp_goroutineleakprofile_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.goroutineleakprofile + +package goexperiment + +const GoroutineLeakProfile = false +const GoroutineLeakProfileInt = 0 diff --git a/src/internal/goexperiment/exp_goroutineleakprofile_on.go b/src/internal/goexperiment/exp_goroutineleakprofile_on.go new file mode 100644 index 00000000000000..28a662eceb46f8 --- /dev/null +++ b/src/internal/goexperiment/exp_goroutineleakprofile_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.goroutineleakprofile + +package goexperiment + +const GoroutineLeakProfile = true +const GoroutineLeakProfileInt = 1 diff --git a/src/internal/goexperiment/flags.go b/src/internal/goexperiment/flags.go index 84dbf594b8792a..232a17135d2cc5 100644 --- a/src/internal/goexperiment/flags.go +++ b/src/internal/goexperiment/flags.go @@ -118,4 +118,7 @@ type Flags struct { // SizeSpecializedMalloc enables malloc implementations that are specialized per size class. SizeSpecializedMalloc bool + + // GoroutineLeakProfile enables the collection of goroutine leak profiles. + GoroutineLeakProfile bool } diff --git a/src/net/http/pprof/pprof.go b/src/net/http/pprof/pprof.go index 635d3ad9d9f132..e5a46ed253cf8b 100644 --- a/src/net/http/pprof/pprof.go +++ b/src/net/http/pprof/pprof.go @@ -77,6 +77,7 @@ import ( "fmt" "html" "internal/godebug" + "internal/goexperiment" "internal/profile" "io" "log" @@ -353,6 +354,7 @@ func collectProfile(p *pprof.Profile) (*profile.Profile, error) { var profileSupportsDelta = map[handler]bool{ "allocs": true, "block": true, + "goroutineleak": true, "goroutine": true, "heap": true, "mutex": true, @@ -372,6 +374,12 @@ var profileDescriptions = map[string]string{ "trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.", } +func init() { + if goexperiment.GoroutineLeakProfile { + profileDescriptions["goroutineleak"] = "Stack traces of all leaked goroutines. Use debug=2 as a query parameter to export in the same format as an unrecovered panic." + } +} + type profileEntry struct { Name string Href string diff --git a/src/runtime/chan.go b/src/runtime/chan.go index 639d29dc8337f0..3320c248b458b6 100644 --- a/src/runtime/chan.go +++ b/src/runtime/chan.go @@ -263,11 +263,11 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { } // No stack splits between assigning elem and enqueuing mysg // on gp.waiting where copystack can find it. - mysg.elem = ep + mysg.elem.set(ep) mysg.waitlink = nil mysg.g = gp mysg.isSelect = false - mysg.c = c + mysg.c.set(c) gp.waiting = mysg gp.param = nil c.sendq.enqueue(mysg) @@ -298,7 +298,7 @@ func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { if mysg.releasetime > 0 { blockevent(mysg.releasetime-t0, 2) } - mysg.c = nil + mysg.c.set(nil) releaseSudog(mysg) if closed { if c.closed == 0 { @@ -336,9 +336,9 @@ func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz } } - if sg.elem != nil { + if sg.elem.get() != nil { sendDirect(c.elemtype, sg, ep) - sg.elem = nil + sg.elem.set(nil) } gp := sg.g unlockf() @@ -395,7 +395,7 @@ func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { // Once we read sg.elem out of sg, it will no longer // be updated if the destination's stack gets copied (shrunk). // So make sure that no preemption points can happen between read & use. - dst := sg.elem + dst := sg.elem.get() typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_) // No need for cgo write barrier checks because dst is always // Go memory. @@ -406,7 +406,7 @@ func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) { // dst is on our stack or the heap, src is on another stack. // The channel is locked, so src will not move during this // operation. - src := sg.elem + src := sg.elem.get() typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_) memmove(dst, src, t.Size_) } @@ -441,9 +441,9 @@ func closechan(c *hchan) { if sg == nil { break } - if sg.elem != nil { - typedmemclr(c.elemtype, sg.elem) - sg.elem = nil + if sg.elem.get() != nil { + typedmemclr(c.elemtype, sg.elem.get()) + sg.elem.set(nil) } if sg.releasetime != 0 { sg.releasetime = cputicks() @@ -463,7 +463,7 @@ func closechan(c *hchan) { if sg == nil { break } - sg.elem = nil + sg.elem.set(nil) if sg.releasetime != 0 { sg.releasetime = cputicks() } @@ -642,13 +642,13 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) } // No stack splits between assigning elem and enqueuing mysg // on gp.waiting where copystack can find it. - mysg.elem = ep + mysg.elem.set(ep) mysg.waitlink = nil gp.waiting = mysg mysg.g = gp mysg.isSelect = false - mysg.c = c + mysg.c.set(c) gp.param = nil c.recvq.enqueue(mysg) if c.timer != nil { @@ -680,7 +680,7 @@ func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) } success := mysg.success gp.param = nil - mysg.c = nil + mysg.c.set(nil) releaseSudog(mysg) return true, success } @@ -727,14 +727,14 @@ func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { typedmemmove(c.elemtype, ep, qp) } // copy data from sender to queue - typedmemmove(c.elemtype, qp, sg.elem) + typedmemmove(c.elemtype, qp, sg.elem.get()) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz } - sg.elem = nil + sg.elem.set(nil) gp := sg.g unlockf() gp.param = unsafe.Pointer(sg) diff --git a/src/runtime/crash_test.go b/src/runtime/crash_test.go index 2db86e0562d6ae..2b8ca549ad84f2 100644 --- a/src/runtime/crash_test.go +++ b/src/runtime/crash_test.go @@ -186,6 +186,22 @@ func buildTestProg(t *testing.T, binary string, flags ...string) (string, error) t.Logf("running %v", cmd) cmd.Dir = "testdata/" + binary cmd = testenv.CleanCmdEnv(cmd) + + // If tests need any experimental flags, add them here. + // + // TODO(vsaioc): Remove `goroutineleakprofile` once the feature is no longer experimental. + edited := false + for i := range cmd.Env { + e := cmd.Env[i] + if _, vars, ok := strings.Cut(e, "GOEXPERIMENT="); ok { + cmd.Env[i] = "GOEXPERIMENT=" + vars + ",goroutineleakprofile" + edited, _ = true, vars + } + } + if !edited { + cmd.Env = append(cmd.Env, "GOEXPERIMENT=goroutineleakprofile") + } + out, err := cmd.CombinedOutput() if err != nil { target.err = fmt.Errorf("building %s %v: %v\n%s", binary, flags, err, out) diff --git a/src/runtime/goroutineleakprofile_test.go b/src/runtime/goroutineleakprofile_test.go new file mode 100644 index 00000000000000..9857d725deef32 --- /dev/null +++ b/src/runtime/goroutineleakprofile_test.go @@ -0,0 +1,595 @@ +// 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 runtime_test + +import ( + "fmt" + "regexp" + "strings" + "testing" +) + +func TestGoroutineLeakProfile(t *testing.T) { + // Goroutine leak test case. + // + // Test cases can be configured with test name, the name of the entry point function, + // a set of expected leaks identified by regular expressions, and the number of times + // the test should be repeated. + // + // Repeated runs reduce flakiness in some tests. + type testCase struct { + name string + simple bool + repetitions int + expectedLeaks map[*regexp.Regexp]bool + + // flakyLeaks are goroutine leaks that are too flaky to be reliably detected. + // Still, they might pop up every once in a while. The test will pass regardless + // if they occur or nor, as they are not unexpected. + // + // Note that all flaky leaks are true positives, i.e. real goroutine leaks, + // and it is only their detection that is unreliable due to scheduling + // non-determinism. + flakyLeaks map[*regexp.Regexp]struct{} + } + + // makeAnyTest is a short-hand for creating test cases. + // Each of the leaks in the list is identified by a regular expression. + // If a leak is flaky, it is added to the flakyLeaks map. + makeAnyTest := func(name string, flaky bool, repetitions int, leaks ...string) testCase { + tc := testCase{ + name: name, + expectedLeaks: make(map[*regexp.Regexp]bool, len(leaks)), + flakyLeaks: make(map[*regexp.Regexp]struct{}, len(leaks)), + // Make sure the test is repeated at least once. + repetitions: repetitions | 1, + } + + for _, leak := range leaks { + if !flaky { + tc.expectedLeaks[regexp.MustCompile(leak)] = false + } else { + tc.flakyLeaks[regexp.MustCompile(leak)] = struct{}{} + } + } + + return tc + } + + // makeTest is a short-hand for creating non-flaky test cases. + makeTest := func(name string, leaks ...string) testCase { + tcase := makeAnyTest(name, false, 2, leaks...) + tcase.simple = true + return tcase + } + + // makeFlakyTest is a short-hand for creating flaky test cases. + makeFlakyTest := func(name string, leaks ...string) testCase { + if testing.Short() { + return makeAnyTest(name, true, 2, leaks...) + } + return makeAnyTest(name, true, 10, leaks...) + } + + goroutineHeader := regexp.MustCompile(`goroutine \d+ \[`) + + // extractLeaks takes the output of a test and splits it into a + // list of strings denoting goroutine leaks. + // + // If the input is: + // + // goroutine 1 [wait reason (leaked)]: + // main.leaked() + // ./testdata/testgoroutineleakprofile/foo.go:37 +0x100 + // created by main.main() + // ./testdata/testgoroutineleakprofile/main.go:10 +0x20 + // + // goroutine 2 [wait reason (leaked)]: + // main.leaked2() + // ./testdata/testgoroutineleakprofile/foo.go:37 +0x100 + // created by main.main() + // ./testdata/testgoroutineleakprofile/main.go:10 +0x20 + // + // The output is (as a list of strings): + // + // leaked() [wait reason] + // leaked2() [wait reason] + extractLeaks := func(output string) []string { + stacks := strings.Split(output, "\n\ngoroutine") + var leaks []string + for _, stack := range stacks { + lines := strings.Split(stack, "\n") + if len(lines) < 5 { + // Expecting at least the following lines (where n=len(lines)-1): + // + // [0] goroutine n [wait reason (leaked)] + // ... + // [n-3] bottom.leak.frame(...) + // [n-2] ./bottom/leak/frame/source.go:line + // [n-1] created by go.instruction() + // [n] ./go/instruction/source.go:line + continue + } + + if !strings.Contains(lines[0], "(leaked)") { + // Ignore non-leaked goroutines. + continue + } + + // Get the wait reason from the goroutine header. + header := lines[0] + waitReason := goroutineHeader.ReplaceAllString(header, "[") + waitReason = strings.ReplaceAll(waitReason, " (leaked)", "") + + // Get the function name from the stack trace (should be two lines above `created by`). + var funcName string + for i := len(lines) - 1; i >= 0; i-- { + if strings.Contains(lines[i], "created by") { + funcName = strings.TrimPrefix(lines[i-2], "main.") + break + } + } + if funcName == "" { + t.Fatalf("failed to extract function name from stack trace: %s", lines) + } + + leaks = append(leaks, funcName+" "+waitReason) + } + return leaks + } + + // Micro tests involve very simple leaks for each type of concurrency primitive operation. + microTests := []testCase{ + makeTest("NilRecv", + `NilRecv\.func1\(.* \[chan receive \(nil chan\)\]`, + ), + makeTest("NilSend", + `NilSend\.func1\(.* \[chan send \(nil chan\)\]`, + ), + makeTest("SelectNoCases", + `SelectNoCases\.func1\(.* \[select \(no cases\)\]`, + ), + makeTest("ChanRecv", + `ChanRecv\.func1\(.* \[chan receive\]`, + ), + makeTest("ChanSend", + `ChanSend\.func1\(.* \[chan send\]`, + ), + makeTest("Select", + `Select\.func1\(.* \[select\]`, + ), + makeTest("WaitGroup", + `WaitGroup\.func1\(.* \[sync\.WaitGroup\.Wait\]`, + ), + makeTest("MutexStack", + `MutexStack\.func1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("MutexHeap", + `MutexHeap\.func1.1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Cond", + `Cond\.func1\(.* \[sync\.Cond\.Wait\]`, + ), + makeTest("RWMutexRLock", + `RWMutexRLock\.func1\(.* \[sync\.RWMutex\.RLock\]`, + ), + makeTest("RWMutexLock", + `RWMutexLock\.func1\(.* \[sync\.(RW)?Mutex\.Lock\]`, + ), + makeTest("Mixed", + `Mixed\.func1\(.* \[sync\.WaitGroup\.Wait\]`, + `Mixed\.func1.1\(.* \[chan send\]`, + ), + makeTest("NoLeakGlobal"), + } + + // Stress tests are flaky and we do not strictly care about their output. + // They are only intended to stress the goroutine leak detector and profiling + // infrastructure in interesting ways. + stressTestCases := []testCase{ + makeFlakyTest("SpawnGC", + `spawnGC.func1\(.* \[chan receive\]`, + ), + makeTest("DaisyChain"), + } + + // Common goroutine leak patterns. + // Extracted from "Unveiling and Vanquishing Goroutine Leaks in Enterprise Microservices: A Dynamic Analysis Approach" + // doi:10.1109/CGO57630.2024.10444835 + patternTestCases := []testCase{ + makeTest("NoCloseRange", + `noCloseRange\(.* \[chan send\]`, + `noCloseRange\.func1\(.* \[chan receive\]`, + ), + makeTest("MethodContractViolation", + `worker\.Start\.func1\(.* \[select\]`, + ), + makeTest("DoubleSend", + `DoubleSend\.func3\(.* \[chan send\]`, + ), + makeTest("EarlyReturn", + `earlyReturn\.func1\(.* \[chan send\]`, + ), + makeTest("NCastLeak", + `nCastLeak\.func1\(.* \[chan send\]`, + `NCastLeak\.func2\(.* \[chan receive\]`, + ), + makeTest("Timeout", + // (vsaioc): Timeout is *theoretically* flaky, but the + // pseudo-random choice for select case branches makes it + // practically impossible for it to fail. + `timeout\.func1\(.* \[chan send\]`, + ), + } + + // GoKer tests from "GoBench: A Benchmark Suite of Real-World Go Concurrency Bugs". + // Refer to testdata/testgoroutineleakprofile/goker/README.md. + // + // This list is curated for tests that are not excessively flaky. + // Some tests are also excluded because they are redundant. + // + // TODO(vsaioc): Some of these might be removable (their patterns may overlap). + gokerTestCases := []testCase{ + makeFlakyTest("Cockroach584", + `Cockroach584\.func2\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Cockroach1055", + `Cockroach1055\.func2\(.* \[chan receive\]`, + `Cockroach1055\.func2\.2\(.* \[sync\.WaitGroup\.Wait\]`, + `Cockroach1055\.func2\.1\(.* \[chan receive\]`, + `Cockroach1055\.func2\.1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Cockroach1462", + `\(\*Stopper_cockroach1462\)\.RunWorker\.func1\(.* \[chan send\]`, + `Cockroach1462\.func2\(.* \[sync\.WaitGroup\.Wait\]`, + ), + makeFlakyTest("Cockroach2448", + `\(\*Store_cockroach2448\)\.processRaft\(.* \[select\]`, + `\(\*state_cockroach2448\)\.start\(.* \[select\]`, + ), + makeFlakyTest("Cockroach3710", + `\(\*Store_cockroach3710\)\.ForceRaftLogScanAndProcess\(.* \[sync\.RWMutex\.RLock\]`, + `\(\*Store_cockroach3710\)\.processRaft\.func1\(.* \[sync\.RWMutex\.Lock\]`, + ), + makeFlakyTest("Cockroach6181", + `testRangeCacheCoalescedRequests_cockroach6181\(.* \[sync\.WaitGroup\.Wait\]`, + `testRangeCacheCoalescedRequests_cockroach6181\.func1\.1\(.* \[sync\.(RW)?Mutex\.Lock\]`, + `testRangeCacheCoalescedRequests_cockroach6181\.func1\.1\(.* \[sync\.RWMutex\.RLock\]`, + ), + makeTest("Cockroach7504", + `Cockroach7504\.func2\.1.* \[sync\.Mutex\.Lock\]`, + `Cockroach7504\.func2\.2.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Cockroach9935", + `\(\*loggingT_cockroach9935\)\.outputLogEntry\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Cockroach10214", + `\(*Store_cockroach10214\)\.sendQueuedHeartbeats\(.* \[sync\.Mutex\.Lock\]`, + `\(*Replica_cockroach10214\)\.tick\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Cockroach10790", + `\(\*Replica_cockroach10790\)\.beginCmds\.func1\(.* \[chan receive\]`, + ), + makeTest("Cockroach13197", + `\(\*Tx_cockroach13197\)\.awaitDone\(.* \[chan receive\]`, + ), + makeTest("Cockroach13755", + `\(\*Rows_cockroach13755\)\.awaitDone\(.* \[chan receive\]`, + ), + makeFlakyTest("Cockroach16167", + `Cockroach16167\.func2\(.* \[sync\.RWMutex\.RLock\]`, + `\(\*Executor_cockroach16167\)\.Start\(.* \[sync\.RWMutex\.Lock\]`, + ), + makeFlakyTest("Cockroach18101", + `restore_cockroach18101\.func1\(.* \[chan send\]`, + ), + makeTest("Cockroach24808", + `Cockroach24808\.func2\(.* \[chan send\]`, + ), + makeTest("Cockroach25456", + `Cockroach25456\.func2\(.* \[chan receive\]`, + ), + makeTest("Cockroach35073", + `Cockroach35073\.func2.1\(.* \[chan send\]`, + `Cockroach35073\.func2\(.* \[chan send\]`, + ), + makeTest("Cockroach35931", + `Cockroach35931\.func2\(.* \[chan send\]`, + ), + makeTest("Etcd5509", + `Etcd5509\.func2\(.* \[sync\.RWMutex\.Lock\]`, + ), + makeTest("Etcd6708", + `Etcd6708\.func2\(.* \[sync\.RWMutex\.RLock\]`, + ), + makeFlakyTest("Etcd6857", + `\(\*node_etcd6857\)\.Status\(.* \[chan send\]`, + ), + makeFlakyTest("Etcd6873", + `\(\*watchBroadcasts_etcd6873\)\.stop\(.* \[chan receive\]`, + `newWatchBroadcasts_etcd6873\.func1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Etcd7492", + `Etcd7492\.func2\(.* \[sync\.WaitGroup\.Wait\]`, + `Etcd7492\.func2\.1\(.* \[chan send\]`, + `\(\*simpleTokenTTLKeeper_etcd7492\)\.run\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Etcd7902", + `doRounds_etcd7902\.func1\(.* \[chan receive\]`, + `doRounds_etcd7902\.func1\(.* \[sync\.Mutex\.Lock\]`, + `runElectionFunc_etcd7902\(.* \[sync\.WaitGroup\.Wait\]`, + ), + makeTest("Etcd10492", + `Etcd10492\.func2\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Grpc660", + `\(\*benchmarkClient_grpc660\)\.doCloseLoopUnary\.func1\(.* \[chan send\]`, + ), + makeFlakyTest("Grpc795", + `\(\*Server_grpc795\)\.Serve\(.* \[sync\.Mutex\.Lock\]`, + `testServerGracefulStopIdempotent_grpc795\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Grpc862", + `DialContext_grpc862\.func2\(.* \[chan receive\]`), + makeTest("Grpc1275", + `testInflightStreamClosing_grpc1275\.func1\(.* \[chan receive\]`), + makeTest("Grpc1424", + `DialContext_grpc1424\.func1\(.* \[chan receive\]`), + makeFlakyTest("Grpc1460", + `\(\*http2Client_grpc1460\)\.keepalive\(.* \[chan receive\]`, + `\(\*http2Client_grpc1460\)\.NewStream\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Grpc3017", + // grpc/3017 involves a goroutine leak that also simultaneously engages many GC assists. + `Grpc3017\.func2\(.* \[chan receive\]`, + `Grpc3017\.func2\.1\(.* \[sync\.Mutex\.Lock\]`, + `\(\*lbCacheClientConn_grpc3017\)\.RemoveSubConn\.func1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Hugo3251", + `Hugo3251\.func2\(.* \[sync\.WaitGroup\.Wait\]`, + `Hugo3251\.func2\.1\(.* \[sync\.Mutex\.Lock\]`, + `Hugo3251\.func2\.1\(.* \[sync\.RWMutex\.RLock\]`, + ), + makeFlakyTest("Hugo5379", + `\(\*Page_hugo5379\)\.initContent\.func1\.1\(.* \[sync\.Mutex\.Lock\]`, + `pageRenderer_hugo5379\(.* \[sync\.Mutex\.Lock\]`, + `Hugo5379\.func2\(.* \[sync\.WaitGroup\.Wait\]`, + ), + makeFlakyTest("Istio16224", + `Istio16224\.func2\(.* \[sync\.Mutex\.Lock\]`, + `\(\*controller_istio16224\)\.Run\(.* \[chan send\]`, + `\(\*controller_istio16224\)\.Run\(.* \[chan receive\]`, + ), + makeFlakyTest("Istio17860", + `\(\*agent_istio17860\)\.runWait\(.* \[chan send\]`, + ), + makeFlakyTest("Istio18454", + `\(\*Worker_istio18454\)\.Start\.func1\(.* \[chan receive\]`, + `\(\*Worker_istio18454\)\.Start\.func1\(.* \[chan send\]`, + ), + // NOTE(vsaioc): + // Kubernetes/1321 is excluded due to a race condition in the original program + // that may, in extremely rare cases, lead to nil pointer dereference crashes. + // (Reproducible even with regular GC). Only kept here for posterity. + // + // makeTest(testCase{name: "Kubernetes1321"}, + // `NewMux_kubernetes1321\.gowrap1\(.* \[chan send\]`, + // `testMuxWatcherClose_kubernetes1321\(.* \[sync\.Mutex\.Lock\]`), + makeTest("Kubernetes5316", + `finishRequest_kubernetes5316\.func1\(.* \[chan send\]`, + ), + makeFlakyTest("Kubernetes6632", + `\(\*idleAwareFramer_kubernetes6632\)\.monitor\(.* \[sync\.Mutex\.Lock\]`, + `\(\*idleAwareFramer_kubernetes6632\)\.WriteFrame\(.* \[chan send\]`, + ), + makeFlakyTest("Kubernetes10182", + `\(\*statusManager_kubernetes10182\)\.Start\.func1\(.* \[sync\.Mutex\.Lock\]`, + `\(\*statusManager_kubernetes10182\)\.SetPodStatus\(.* \[chan send\]`, + ), + makeFlakyTest("Kubernetes11298", + `After_kubernetes11298\.func1\(.* \[chan receive\]`, + `After_kubernetes11298\.func1\(.* \[sync\.Cond\.Wait\]`, + `Kubernetes11298\.func2\(.* \[chan receive\]`, + ), + makeFlakyTest("Kubernetes13135", + `Util_kubernetes13135\(.* \[sync\.Mutex\.Lock\]`, + `\(\*WatchCache_kubernetes13135\)\.Add\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Kubernetes25331", + `\(\*watchChan_kubernetes25331\)\.run\(.* \[chan send\]`, + ), + makeFlakyTest("Kubernetes26980", + `Kubernetes26980\.func2\(.* \[chan receive\]`, + `Kubernetes26980\.func2\.1\(.* \[sync\.Mutex\.Lock\]`, + `\(\*processorListener_kubernetes26980\)\.pop\(.* \[chan receive\]`, + ), + makeFlakyTest("Kubernetes30872", + `\(\*DelayingDeliverer_kubernetes30872\)\.StartWithHandler\.func1\(.* \[sync\.Mutex\.Lock\]`, + `\(\*Controller_kubernetes30872\)\.Run\(.* \[sync\.Mutex\.Lock\]`, + `\(\*NamespaceController_kubernetes30872\)\.Run\.func1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Kubernetes38669", + `\(\*cacheWatcher_kubernetes38669\)\.process\(.* \[chan send\]`, + ), + makeFlakyTest("Kubernetes58107", + `\(\*ResourceQuotaController_kubernetes58107\)\.worker\(.* \[sync\.Cond\.Wait\]`, + `\(\*ResourceQuotaController_kubernetes58107\)\.worker\(.* \[sync\.RWMutex\.RLock\]`, + `\(\*ResourceQuotaController_kubernetes58107\)\.Sync\(.* \[sync\.RWMutex\.Lock\]`, + ), + makeFlakyTest("Kubernetes62464", + `\(\*manager_kubernetes62464\)\.reconcileState\(.* \[sync\.RWMutex\.RLock\]`, + `\(\*staticPolicy_kubernetes62464\)\.RemoveContainer\(.* \[sync\.(RW)?Mutex\.Lock\]`, + ), + makeFlakyTest("Kubernetes70277", + `Kubernetes70277\.func2\(.* \[chan receive\]`, + ), + makeFlakyTest("Moby4951", + `\(\*DeviceSet_moby4951\)\.DeleteDevice\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Moby7559", + `\(\*UDPProxy_moby7559\)\.Run\(.* \[sync\.Mutex\.Lock\]`, + ), + makeTest("Moby17176", + `testDevmapperLockReleasedDeviceDeletion_moby17176\.func1\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Moby21233", + `\(\*Transfer_moby21233\)\.Watch\.func1\(.* \[chan send\]`, + `\(\*Transfer_moby21233\)\.Watch\.func1\(.* \[select\]`, + `testTransfer_moby21233\(.* \[chan receive\]`, + ), + makeTest("Moby25348", + `\(\*Manager_moby25348\)\.init\(.* \[sync\.WaitGroup\.Wait\]`, + ), + makeFlakyTest("Moby27782", + `\(\*JSONFileLogger_moby27782\)\.readLogs\(.* \[sync\.Cond\.Wait\]`, + `\(\*Watcher_moby27782\)\.readEvents\(.* \[select\]`, + ), + makeFlakyTest("Moby28462", + `monitor_moby28462\(.* \[sync\.Mutex\.Lock\]`, + `\(\*Daemon_moby28462\)\.StateChanged\(.* \[chan send\]`, + ), + makeTest("Moby30408", + `Moby30408\.func2\(.* \[chan receive\]`, + `testActive_moby30408\.func1\(.* \[sync\.Cond\.Wait\]`, + ), + makeFlakyTest("Moby33781", + `monitor_moby33781\.func1\(.* \[chan send\]`, + ), + makeFlakyTest("Moby36114", + `\(\*serviceVM_moby36114\)\.hotAddVHDsAtStart\(.* \[sync\.Mutex\.Lock\]`, + ), + makeFlakyTest("Serving2137", + `\(\*Breaker_serving2137\)\.concurrentRequest\.func1\(.* \[chan send\]`, + `\(\*Breaker_serving2137\)\.concurrentRequest\.func1\(.* \[sync\.Mutex\.Lock\]`, + `Serving2137\.func2\(.* \[chan receive\]`, + ), + makeTest("Syncthing4829", + `Syncthing4829\.func2\(.* \[sync\.RWMutex\.RLock\]`, + ), + makeTest("Syncthing5795", + `\(\*rawConnection_syncthing5795\)\.dispatcherLoop\(.* \[chan receive\]`, + `Syncthing5795\.func2.* \[chan receive\]`, + ), + } + + // Combine all test cases into a single list. + testCases := append(microTests, stressTestCases...) + testCases = append(testCases, patternTestCases...) + + // Test cases must not panic or cause fatal exceptions. + failStates := regexp.MustCompile(`fatal|panic`) + + testApp := func(exepath string, testCases []testCase) { + + // Build the test program once. + exe, err := buildTestProg(t, exepath) + if err != nil { + t.Fatal(fmt.Sprintf("building testgoroutineleakprofile failed: %v", err)) + } + + for _, tcase := range testCases { + t.Run(tcase.name, func(t *testing.T) { + t.Parallel() + + cmdEnv := []string{ + "GODEBUG=asyncpreemptoff=1", + "GOEXPERIMENT=greenteagc,goroutineleakprofile", + } + + if tcase.simple { + // If the test is simple, set GOMAXPROCS=1 in order to better + // control the behavior of the scheduler. + cmdEnv = append(cmdEnv, "GOMAXPROCS=1") + } + + var output string + for i := 0; i < tcase.repetitions; i++ { + // Run program for one repetition and get runOutput trace. + runOutput := runBuiltTestProg(t, exe, tcase.name, cmdEnv...) + if len(runOutput) == 0 { + t.Errorf("Test %s produced no output. Is the goroutine leak profile collected?", tcase.name) + } + + // Zero tolerance policy for fatal exceptions or panics. + if failStates.MatchString(runOutput) { + t.Errorf("unexpected fatal exception or panic!\noutput:\n%s\n\n", runOutput) + } + + output += runOutput + "\n\n" + } + + // Extract all the goroutine leaks + foundLeaks := extractLeaks(output) + + // If the test case was not expected to produce leaks, but some were reported, + // stop the test immediately. Zero tolerance policy for false positives. + if len(tcase.expectedLeaks)+len(tcase.flakyLeaks) == 0 && len(foundLeaks) > 0 { + t.Errorf("output:\n%s\n\ngoroutines leaks detected in case with no leaks", output) + } + + unexpectedLeaks := make([]string, 0, len(foundLeaks)) + + // Parse every leak and check if it is expected (maybe as a flaky leak). + LEAKS: + for _, leak := range foundLeaks { + // Check if the leak is expected. + // If it is, check whether it has been encountered before. + var foundNew bool + var leakPattern *regexp.Regexp + + for expectedLeak, ok := range tcase.expectedLeaks { + if expectedLeak.MatchString(leak) { + if !ok { + foundNew = true + } + + leakPattern = expectedLeak + break + } + } + + if foundNew { + // Only bother writing if we found a new leak. + tcase.expectedLeaks[leakPattern] = true + } + + if leakPattern == nil { + // We are dealing with a leak not marked as expected. + // Check if it is a flaky leak. + for flakyLeak := range tcase.flakyLeaks { + if flakyLeak.MatchString(leak) { + // The leak is flaky. Carry on to the next line. + continue LEAKS + } + } + + unexpectedLeaks = append(unexpectedLeaks, leak) + } + } + + missingLeakStrs := make([]string, 0, len(tcase.expectedLeaks)) + for expectedLeak, found := range tcase.expectedLeaks { + if !found { + missingLeakStrs = append(missingLeakStrs, expectedLeak.String()) + } + } + + var errors []error + if len(unexpectedLeaks) > 0 { + errors = append(errors, fmt.Errorf("unexpected goroutine leaks:\n%s\n", strings.Join(unexpectedLeaks, "\n"))) + } + if len(missingLeakStrs) > 0 { + errors = append(errors, fmt.Errorf("missing expected leaks:\n%s\n", strings.Join(missingLeakStrs, ", "))) + } + if len(errors) > 0 { + t.Fatalf("Failed with the following errors:\n%s\n\noutput:\n%s", errors, output) + } + }) + } + } + + testApp("testgoroutineleakprofile", testCases) + testApp("testgoroutineleakprofile/goker", gokerTestCases) +} diff --git a/src/runtime/mbitmap.go b/src/runtime/mbitmap.go index 508de9a115723c..37a92c64bc6ecc 100644 --- a/src/runtime/mbitmap.go +++ b/src/runtime/mbitmap.go @@ -1267,6 +1267,28 @@ func markBitsForSpan(base uintptr) (mbits markBits) { return mbits } +// isMarkedOrNotInHeap returns true if a pointer is in the heap and marked, +// or if the pointer is not in the heap. Used by goroutine leak detection +// to determine if concurrency resources are reachable in memory. +func isMarkedOrNotInHeap(p unsafe.Pointer) bool { + obj, span, objIndex := findObject(uintptr(p), 0, 0) + if obj != 0 { + mbits := span.markBitsForIndex(objIndex) + return mbits.isMarked() + } + + // If we fall through to get here, the object is not in the heap. + // In this case, it is either a pointer to a stack object or a global resource. + // Treat it as reachable in memory by default, to be safe. + // + // TODO(vsaioc): we could be more precise by checking against the stacks + // of runnable goroutines. I don't think this is necessary, based on what we've seen, but + // let's keep the option open in case the runtime evolves. + // This will (naively) lead to quadratic blow-up for goroutine leak detection, + // but if it is only run on demand, maybe the extra cost is not a show-stopper. + return true +} + // advance advances the markBits to the next object in the span. func (m *markBits) advance() { if m.mask == 1<<7 { diff --git a/src/runtime/mgc.go b/src/runtime/mgc.go index 68cbfda5000573..b13ec845fc401f 100644 --- a/src/runtime/mgc.go +++ b/src/runtime/mgc.go @@ -376,8 +376,8 @@ type workType struct { // (and thus 8-byte alignment even on 32-bit architectures). bytesMarked uint64 - markrootNext uint32 // next markroot job - markrootJobs uint32 // number of markroot jobs + markrootNext atomic.Uint32 // next markroot job + markrootJobs atomic.Uint32 // number of markroot jobs nproc uint32 tstart int64 @@ -385,17 +385,44 @@ type workType struct { // Number of roots of various root types. Set by gcPrepareMarkRoots. // - // nStackRoots == len(stackRoots), but we have nStackRoots for - // consistency. - nDataRoots, nBSSRoots, nSpanRoots, nStackRoots int + // During normal GC cycle, nStackRoots == nMaybeRunnableStackRoots == len(stackRoots); + // during goroutine leak detection, nMaybeRunnableStackRoots is the number of stackRoots + // scheduled for marking. + // In both variants, nStackRoots == len(stackRoots). + nDataRoots, nBSSRoots, nSpanRoots, nStackRoots, nMaybeRunnableStackRoots int + + // The following fields monitor the GC phase of the current cycle during + // goroutine leak detection. + goroutineLeak struct { + // Once set, it indicates that the GC will perform goroutine leak detection during + // the next GC cycle; it is set by goroutineLeakGC and unset during gcStart. + pending atomic.Bool + // Once set, it indicates that the GC has started a goroutine leak detection run; + // it is set during gcStart and unset during gcMarkTermination; + // + // Protected by STW. + enabled bool + // Once set, it indicates that the GC has performed goroutine leak detection during + // the current GC cycle; it is set during gcMarkDone, right after goroutine leak detection, + // and unset during gcMarkTermination; + // + // Protected by STW. + done bool + // The number of leaked goroutines during the last leak detection GC cycle. + // + // Write-protected by STW in findGoroutineLeaks. + count int + } // Base indexes of each root type. Set by gcPrepareMarkRoots. baseData, baseBSS, baseSpans, baseStacks, baseEnd uint32 - // stackRoots is a snapshot of all of the Gs that existed - // before the beginning of concurrent marking. The backing - // store of this must not be modified because it might be - // shared with allgs. + // stackRoots is a snapshot of all of the Gs that existed before the + // beginning of concurrent marking. During goroutine leak detection, stackRoots + // is partitioned into two sets; to the left of nMaybeRunnableStackRoots are stackRoots + // of running / runnable goroutines and to the right of nMaybeRunnableStackRoots are + // stackRoots of unmarked / not runnable goroutines + // The stackRoots array is re-partitioned after each marking phase iteration. stackRoots []*g // Each type of GC state transition is protected by a lock. @@ -562,6 +589,55 @@ func GC() { releasem(mp) } +// goroutineLeakGC runs a GC cycle that performs goroutine leak detection. +// +//go:linkname goroutineLeakGC runtime/pprof.runtime_goroutineLeakGC +func goroutineLeakGC() { + // Set the pending flag to true, instructing the next GC cycle to + // perform goroutine leak detection. + work.goroutineLeak.pending.Store(true) + + // Spin GC cycles until the pending flag is unset. + // This ensures that goroutineLeakGC waits for a GC cycle that + // actually performs goroutine leak detection. + // + // This is needed in case multiple concurrent calls to GC + // are simultaneously fired by the system, wherein some + // of them are dropped. + // + // In the vast majority of cases, only one loop iteration is needed; + // however, multiple concurrent calls to goroutineLeakGC could lead to + // the execution of additional GC cycles. + // + // Examples: + // + // pending? | G1 | G2 + // ---------|-------------------------|----------------------- + // - | goroutineLeakGC() | goroutineLeakGC() + // - | pending.Store(true) | . + // X | for pending.Load() | . + // X | GC() | . + // X | > gcStart() | . + // X | pending.Store(false) | . + // ... + // - | > gcMarkDone() | . + // - | . | pending.Store(true) + // ... + // X | > gcMarkTermination() | . + // X | ... + // X | < GC returns | . + // X | for pending.Load | . + // X | GC() | . + // X | . | for pending.Load() + // X | . | GC() + // ... + // The first to pick up the pending flag will start a + // leak detection cycle. + for work.goroutineLeak.pending.Load() { + GC() + } +} + // gcWaitOnMark blocks until GC finishes the Nth mark phase. If GC has // already completed this mark phase, it returns immediately. func gcWaitOnMark(n uint32) { @@ -785,6 +861,15 @@ func gcStart(trigger gcTrigger) { schedEnableUser(false) } + // If goroutine leak detection is pending, enable it for this GC cycle. + if work.goroutineLeak.pending.Load() { + work.goroutineLeak.enabled = true + work.goroutineLeak.pending.Store(false) + // Set all sync objects of blocked goroutines as untraceable + // by the GC. Only set as traceable at the end of the GC cycle. + setSyncObjectsUntraceable() + } + // Enter concurrent mark phase and enable // write barriers. // @@ -995,8 +1080,20 @@ top: } } }) - if restart { - gcDebugMarkDone.restartedDueTo27993 = true + + // Check whether we need to resume the marking phase because of issue #27993 + // or because of goroutine leak detection. + if restart || (work.goroutineLeak.enabled && !work.goroutineLeak.done) { + if restart { + // Restart because of issue #27993. + gcDebugMarkDone.restartedDueTo27993 = true + } else { + // Marking has reached a fixed-point. Attempt to detect goroutine leaks. + // + // If the returned value is true, then detection already concluded for this cycle. + // Otherwise, more runnable goroutines were discovered, requiring additional mark work. + work.goroutineLeak.done = findGoroutineLeaks() + } getg().m.preemptoff = "" systemstack(func() { @@ -1047,6 +1144,172 @@ top: gcMarkTermination(stw) } +// isMaybeRunnable checks whether a goroutine may still be semantically runnable. +// For goroutines which are semantically runnable, this will eventually return true +// as the GC marking phase progresses. It returns false for leaked goroutines, or for +// goroutines which are not yet computed as possibly runnable by the GC. +func (gp *g) isMaybeRunnable() bool { + // Check whether the goroutine is actually in a waiting state first. + if readgstatus(gp) != _Gwaiting { + // If the goroutine is not waiting, then clearly it is maybe runnable. + return true + } + + switch gp.waitreason { + case waitReasonSelectNoCases, + waitReasonChanSendNilChan, + waitReasonChanReceiveNilChan: + // Select with no cases or communicating on nil channels + // make goroutines unrunnable by definition. + return false + case waitReasonChanReceive, + waitReasonSelect, + waitReasonChanSend: + // Cycle all through all *sudog to check whether + // the goroutine is waiting on a marked channel. + for sg := gp.waiting; sg != nil; sg = sg.waitlink { + if isMarkedOrNotInHeap(unsafe.Pointer(sg.c.get())) { + return true + } + } + return false + case waitReasonSyncCondWait, + waitReasonSyncWaitGroupWait, + waitReasonSyncMutexLock, + waitReasonSyncRWMutexLock, + waitReasonSyncRWMutexRLock: + // If waiting on mutexes, wait groups, or condition variables, + // check if the synchronization primitive attached to the sudog is marked. + if gp.waiting != nil { + return isMarkedOrNotInHeap(gp.waiting.elem.get()) + } + } + return true +} + +// findMaybeRunnableGoroutines checks to see if more blocked but maybe-runnable goroutines exist. +// If so, it adds them into root set and increments work.markrootJobs accordingly. +// Returns true if we need to run another phase of markroots; returns false otherwise. +func findMaybeRunnableGoroutines() (moreWork bool) { + oldRootJobs := work.markrootJobs.Load() + + // To begin with we have a set of unchecked stackRoots between + // vIndex and ivIndex. During the loop, anything < vIndex should be + // valid stackRoots and anything >= ivIndex should be invalid stackRoots. + // The loop terminates when the two indices meet. + var vIndex, ivIndex int = work.nMaybeRunnableStackRoots, work.nStackRoots + // Reorder goroutine list + for vIndex < ivIndex { + if work.stackRoots[vIndex].isMaybeRunnable() { + vIndex = vIndex + 1 + continue + } + for ivIndex = ivIndex - 1; ivIndex != vIndex; ivIndex = ivIndex - 1 { + if gp := work.stackRoots[ivIndex]; gp.isMaybeRunnable() { + work.stackRoots[ivIndex] = work.stackRoots[vIndex] + work.stackRoots[vIndex] = gp + vIndex = vIndex + 1 + break + } + } + } + + newRootJobs := work.baseStacks + uint32(vIndex) + if newRootJobs > oldRootJobs { + work.nMaybeRunnableStackRoots = vIndex + work.markrootJobs.Store(newRootJobs) + } + return newRootJobs > oldRootJobs +} + +// setSyncObjectsUntraceable scans allgs and sets the elem and c fields of all sudogs to +// an untrackable pointer. This prevents the GC from marking these objects as live in memory +// by following these pointers when runnning deadlock detection. +func setSyncObjectsUntraceable() { + assertWorldStopped() + + forEachGRace(func(gp *g) { + // Set as untraceable all synchronization objects of goroutines + // blocked at concurrency operations that could leak. + switch { + case gp.waitreason.isSyncWait(): + // Synchronization primitives are reachable from the *sudog via + // via the elem field. + for sg := gp.waiting; sg != nil; sg = sg.waitlink { + sg.elem.setUntraceable() + } + case gp.waitreason.isChanWait(): + // Channels and select statements are reachable from the *sudog via the c field. + for sg := gp.waiting; sg != nil; sg = sg.waitlink { + sg.c.setUntraceable() + } + } + }) +} + +// gcRestoreSyncObjects restores the elem and c fields of all sudogs to their original values. +// Should be invoked after the goroutine leak detection phase. +func gcRestoreSyncObjects() { + assertWorldStopped() + + forEachGRace(func(gp *g) { + for sg := gp.waiting; sg != nil; sg = sg.waitlink { + sg.elem.setTraceable() + sg.c.setTraceable() + } + }) +} + +// findGoroutineLeaks scans the remaining stackRoots and marks any which are +// blocked over exclusively unreachable concurrency primitives as leaked (deadlocked). +// Returns true if the goroutine leak check was performed (or unnecessary). +// Returns false if the GC cycle has not yet computed all maybe-runnable goroutines. +func findGoroutineLeaks() bool { + assertWorldStopped() + + // Report goroutine leaks and mark them unreachable, and resume marking + // we still need to mark these unreachable *g structs as they + // get reused, but their stack won't get scanned + if work.nMaybeRunnableStackRoots == work.nStackRoots { + // nMaybeRunnableStackRoots == nStackRoots means that all goroutines are marked. + return true + } + + // Check whether any more maybe-runnable goroutines can be found by the GC. + if findMaybeRunnableGoroutines() { + // We found more work, so we need to resume the marking phase. + return false + } + + // For the remaining goroutines, mark them as unreachable and leaked. + work.goroutineLeak.count = work.nStackRoots - work.nMaybeRunnableStackRoots + + for i := work.nMaybeRunnableStackRoots; i < work.nStackRoots; i++ { + gp := work.stackRoots[i] + casgstatus(gp, _Gwaiting, _Gleaked) + + // Add the primitives causing the goroutine leaks + // to the GC work queue, to ensure they are marked. + // + // NOTE(vsaioc): these primitives should also be reachable + // from the goroutine's stack, but let's play it safe. + switch { + case gp.waitreason.isChanWait(): + for sg := gp.waiting; sg != nil; sg = sg.waitlink { + shade(sg.c.uintptr()) + } + case gp.waitreason.isSyncWait(): + for sg := gp.waiting; sg != nil; sg = sg.waitlink { + shade(sg.elem.uintptr()) + } + } + } + // Put the remaining roots as ready for marking and drain them. + work.markrootJobs.Add(int32(work.nStackRoots - work.nMaybeRunnableStackRoots)) + work.nMaybeRunnableStackRoots = work.nStackRoots + return true +} + // World must be stopped and mark assists and background workers must be // disabled. func gcMarkTermination(stw worldStop) { @@ -1199,7 +1462,18 @@ func gcMarkTermination(stw worldStop) { throw("non-concurrent sweep failed to drain all sweep queues") } + if work.goroutineLeak.enabled { + // Restore the elem and c fields of all sudogs to their original values. + gcRestoreSyncObjects() + } + + var goroutineLeakDone bool systemstack(func() { + // Pull the GC out of goroutine leak detection mode. + work.goroutineLeak.enabled = false + goroutineLeakDone = work.goroutineLeak.done + work.goroutineLeak.done = false + // The memstats updated above must be updated with the world // stopped to ensure consistency of some values, such as // sched.idleTime and sched.totaltime. memstats also include @@ -1273,7 +1547,11 @@ func gcMarkTermination(stw worldStop) { printlock() print("gc ", memstats.numgc, " @", string(itoaDiv(sbuf[:], uint64(work.tSweepTerm-runtimeInitTime)/1e6, 3)), "s ", - util, "%: ") + util, "%") + if goroutineLeakDone { + print(" (checking for goroutine leaks)") + } + print(": ") prev := work.tSweepTerm for i, ns := range []int64{work.tMark, work.tMarkTerm, work.tEnd} { if i != 0 { @@ -1647,8 +1925,8 @@ func gcMark(startTime int64) { work.tstart = startTime // Check that there's no marking work remaining. - if work.full != 0 || work.markrootNext < work.markrootJobs { - print("runtime: full=", hex(work.full), " next=", work.markrootNext, " jobs=", work.markrootJobs, " nDataRoots=", work.nDataRoots, " nBSSRoots=", work.nBSSRoots, " nSpanRoots=", work.nSpanRoots, " nStackRoots=", work.nStackRoots, "\n") + if next, jobs := work.markrootNext.Load(), work.markrootJobs.Load(); work.full != 0 || next < jobs { + print("runtime: full=", hex(work.full), " next=", next, " jobs=", jobs, " nDataRoots=", work.nDataRoots, " nBSSRoots=", work.nBSSRoots, " nSpanRoots=", work.nSpanRoots, " nStackRoots=", work.nStackRoots, "\n") panic("non-empty mark queue after concurrent mark") } diff --git a/src/runtime/mgcmark.go b/src/runtime/mgcmark.go index bb5fcabab37db8..ba3824f00dc9cb 100644 --- a/src/runtime/mgcmark.go +++ b/src/runtime/mgcmark.go @@ -53,6 +53,55 @@ const ( pagesPerSpanRoot = min(512, pagesPerArena) ) +// internalBlocked returns true if the goroutine is blocked due to an +// internal (non-leaking) waitReason, e.g. waiting for the netpoller or garbage collector. +// Such goroutines are never leak detection candidates according to the GC. +// +//go:nosplit +func (gp *g) internalBlocked() bool { + reason := gp.waitreason + return reason < waitReasonChanReceiveNilChan || waitReasonSyncWaitGroupWait < reason +} + +// allGsSnapshotSortedForGC takes a snapshot of allgs and returns a sorted +// array of Gs. The array is sorted by the G's status, with running Gs +// first, followed by blocked Gs. The returned index indicates the cutoff +// between runnable and blocked Gs. +// +// The world must be stopped or allglock must be held. +func allGsSnapshotSortedForGC() ([]*g, int) { + assertWorldStoppedOrLockHeld(&allglock) + + // Reset the status of leaked goroutines in order to improve + // the precision of goroutine leak detection. + for _, gp := range allgs { + gp.atomicstatus.CompareAndSwap(_Gleaked, _Gwaiting) + } + + allgsSorted := make([]*g, len(allgs)) + + // Indices cutting off runnable and blocked Gs. + var currIndex, blockedIndex = 0, len(allgsSorted) - 1 + for _, gp := range allgs { + // not sure if we need atomic load because we are stopping the world, + // but do it just to be safe for now + if status := readgstatus(gp); status != _Gwaiting || gp.internalBlocked() { + allgsSorted[currIndex] = gp + currIndex++ + } else { + allgsSorted[blockedIndex] = gp + blockedIndex-- + } + } + + // Because the world is stopped or allglock is held, allgadd + // cannot happen concurrently with this. allgs grows + // monotonically and existing entries never change, so we can + // simply return a copy of the slice header. For added safety, + // we trim everything past len because that can still change. + return allgsSorted, blockedIndex + 1 +} + // gcPrepareMarkRoots queues root scanning jobs (stacks, globals, and // some miscellany) and initializes scanning-related state. // @@ -102,11 +151,20 @@ func gcPrepareMarkRoots() { // ignore them because they begin life without any roots, so // there's nothing to scan, and any roots they create during // the concurrent phase will be caught by the write barrier. - work.stackRoots = allGsSnapshot() + if work.goroutineLeak.enabled { + // goroutine leak finder GC --- only prepare runnable + // goroutines for marking. + work.stackRoots, work.nMaybeRunnableStackRoots = allGsSnapshotSortedForGC() + } else { + // regular GC --- scan every goroutine + work.stackRoots = allGsSnapshot() + work.nMaybeRunnableStackRoots = len(work.stackRoots) + } + work.nStackRoots = len(work.stackRoots) - work.markrootNext = 0 - work.markrootJobs = uint32(fixedRootCount + work.nDataRoots + work.nBSSRoots + work.nSpanRoots + work.nStackRoots) + work.markrootNext.Store(0) + work.markrootJobs.Store(uint32(fixedRootCount + work.nDataRoots + work.nBSSRoots + work.nSpanRoots + work.nMaybeRunnableStackRoots)) // Calculate base indexes of each root type work.baseData = uint32(fixedRootCount) @@ -119,8 +177,8 @@ func gcPrepareMarkRoots() { // gcMarkRootCheck checks that all roots have been scanned. It is // purely for debugging. func gcMarkRootCheck() { - if work.markrootNext < work.markrootJobs { - print(work.markrootNext, " of ", work.markrootJobs, " markroot jobs done\n") + if next, jobs := work.markrootNext.Load(), work.markrootJobs.Load(); next < jobs { + print(next, " of ", jobs, " markroot jobs done\n") throw("left over markroot jobs") } @@ -858,7 +916,7 @@ func scanstack(gp *g, gcw *gcWork) int64 { case _Grunning: print("runtime: gp=", gp, ", goid=", gp.goid, ", gp->atomicstatus=", readgstatus(gp), "\n") throw("scanstack: goroutine not stopped") - case _Grunnable, _Gsyscall, _Gwaiting: + case _Grunnable, _Gsyscall, _Gwaiting, _Gleaked: // ok } @@ -1126,6 +1184,28 @@ func gcDrainMarkWorkerFractional(gcw *gcWork) { gcDrain(gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit) } +// gcNextMarkRoot safely increments work.markrootNext and returns the +// index of the next root job. The returned boolean is true if the root job +// is valid, and false if there are no more root jobs to be claimed, +// i.e. work.markrootNext >= work.markrootJobs. +func gcNextMarkRoot() (uint32, bool) { + if !work.goroutineLeak.enabled { + // If not running goroutine leak detection, assume regular GC behavior. + job := work.markrootNext.Add(1) - 1 + return job, job < work.markrootJobs.Load() + } + + // Otherwise, use a CAS loop to increment markrootNext. + for next, jobs := work.markrootNext.Load(), work.markrootJobs.Load(); next < jobs; next = work.markrootNext.Load() { + // There is still work available at the moment. + if work.markrootNext.CompareAndSwap(next, next+1) { + // We manage to snatch a root job. Return the root index. + return next, true + } + } + return 0, false +} + // gcDrain scans roots and objects in work buffers, blackening grey // objects until it is unable to get more work. It may return before // GC is done; it's the caller's responsibility to balance work from @@ -1184,13 +1264,12 @@ func gcDrain(gcw *gcWork, flags gcDrainFlags) { } } - // Drain root marking jobs. - if work.markrootNext < work.markrootJobs { + if work.markrootNext.Load() < work.markrootJobs.Load() { // Stop if we're preemptible, if someone wants to STW, or if // someone is calling forEachP. for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0)) { - job := atomic.Xadd(&work.markrootNext, +1) - 1 - if job >= work.markrootJobs { + job, ok := gcNextMarkRoot() + if !ok { break } markroot(gcw, job, flushBgCredit) @@ -1342,9 +1421,9 @@ func gcDrainN(gcw *gcWork, scanWork int64) int64 { if b = gcw.tryGetObj(); b == 0 { if s = gcw.tryGetSpan(); s == 0 { // Try to do a root job. - if work.markrootNext < work.markrootJobs { - job := atomic.Xadd(&work.markrootNext, +1) - 1 - if job < work.markrootJobs { + if work.markrootNext.Load() < work.markrootJobs.Load() { + job, ok := gcNextMarkRoot() + if ok { workFlushed += markroot(gcw, job, false) continue } diff --git a/src/runtime/mgcmark_greenteagc.go b/src/runtime/mgcmark_greenteagc.go index 53fcd3d966ec3b..3975e1e76b501c 100644 --- a/src/runtime/mgcmark_greenteagc.go +++ b/src/runtime/mgcmark_greenteagc.go @@ -1143,7 +1143,7 @@ func gcMarkWorkAvailable() bool { if !work.full.empty() { return true // global work available } - if work.markrootNext < work.markrootJobs { + if work.markrootNext.Load() < work.markrootJobs.Load() { return true // root scan work available } if work.spanqMask.any() { diff --git a/src/runtime/mgcmark_nogreenteagc.go b/src/runtime/mgcmark_nogreenteagc.go index e4505032917210..9838887f7be008 100644 --- a/src/runtime/mgcmark_nogreenteagc.go +++ b/src/runtime/mgcmark_nogreenteagc.go @@ -124,7 +124,7 @@ func gcMarkWorkAvailable() bool { if !work.full.empty() { return true // global work available } - if work.markrootNext < work.markrootJobs { + if work.markrootNext.Load() < work.markrootJobs.Load() { return true // root scan work available } return false diff --git a/src/runtime/mprof.go b/src/runtime/mprof.go index 97b29076523d5d..0957e67b50fa2c 100644 --- a/src/runtime/mprof.go +++ b/src/runtime/mprof.go @@ -1259,6 +1259,20 @@ func goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.P return goroutineProfileWithLabelsConcurrent(p, labels) } +//go:linkname pprof_goroutineLeakProfileWithLabels +func pprof_goroutineLeakProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) { + return goroutineLeakProfileWithLabelsConcurrent(p, labels) +} + +// labels may be nil. If labels is non-nil, it must have the same length as p. +func goroutineLeakProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) { + if labels != nil && len(labels) != len(p) { + labels = nil + } + + return goroutineLeakProfileWithLabelsConcurrent(p, labels) +} + var goroutineProfile = struct { sema uint32 active bool @@ -1302,6 +1316,48 @@ func (p *goroutineProfileStateHolder) CompareAndSwap(old, new goroutineProfileSt return (*atomic.Uint32)(p).CompareAndSwap(uint32(old), uint32(new)) } +func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) { + if len(p) == 0 { + // An empty slice is obviously too small. Return a rough + // allocation estimate. + return work.goroutineLeak.count, false + } + + // Use the same semaphore as goroutineProfileWithLabelsConcurrent, + // because ultimately we still use goroutine profiles. + semacquire(&goroutineProfile.sema) + + // Unlike in goroutineProfileWithLabelsConcurrent, we don't need to + // save the current goroutine stack, because it is obviously not leaked. + + pcbuf := makeProfStack() // see saveg() for explanation + + // Prepare a profile large enough to store all leaked goroutines. + n = work.goroutineLeak.count + + if n > len(p) { + // There's not enough space in p to store the whole profile, so (per the + // contract of runtime.GoroutineProfile) we're not allowed to write to p + // at all and must return n, false. + semrelease(&goroutineProfile.sema) + return n, false + } + + // Visit each leaked goroutine and try to record its stack. + forEachGRace(func(gp1 *g) { + if readgstatus(gp1) == _Gleaked { + doRecordGoroutineProfile(gp1, pcbuf) + } + }) + + if raceenabled { + raceacquire(unsafe.Pointer(&labelSync)) + } + + semrelease(&goroutineProfile.sema) + return n, true +} + func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) { if len(p) == 0 { // An empty slice is obviously too small. Return a rough diff --git a/src/runtime/pprof/pprof.go b/src/runtime/pprof/pprof.go index 55563009b3a2c1..b524e992b8b209 100644 --- a/src/runtime/pprof/pprof.go +++ b/src/runtime/pprof/pprof.go @@ -80,6 +80,7 @@ import ( "cmp" "fmt" "internal/abi" + "internal/goexperiment" "internal/profilerecord" "io" "runtime" @@ -105,12 +106,13 @@ import ( // // Each Profile has a unique name. A few profiles are predefined: // -// goroutine - stack traces of all current goroutines -// heap - a sampling of memory allocations of live objects -// allocs - a sampling of all past memory allocations -// threadcreate - stack traces that led to the creation of new OS threads -// block - stack traces that led to blocking on synchronization primitives -// mutex - stack traces of holders of contended mutexes +// goroutine - stack traces of all current goroutines +// goroutineleak - stack traces of all leaked goroutines +// allocs - a sampling of all past memory allocations +// heap - a sampling of memory allocations of live objects +// threadcreate - stack traces that led to the creation of new OS threads +// block - stack traces that led to blocking on synchronization primitives +// mutex - stack traces of holders of contended mutexes // // These predefined profiles maintain themselves and panic on an explicit // [Profile.Add] or [Profile.Remove] method call. @@ -169,6 +171,7 @@ import ( // holds a lock for 1s while 5 other goroutines are waiting for the entire // second to acquire the lock, its unlock call stack will report 5s of // contention. + type Profile struct { name string mu sync.Mutex @@ -189,6 +192,12 @@ var goroutineProfile = &Profile{ write: writeGoroutine, } +var goroutineLeakProfile = &Profile{ + name: "goroutineleak", + count: runtime_goroutineleakcount, + write: writeGoroutineLeak, +} + var threadcreateProfile = &Profile{ name: "threadcreate", count: countThreadCreate, @@ -231,6 +240,9 @@ func lockProfiles() { "block": blockProfile, "mutex": mutexProfile, } + if goexperiment.GoroutineLeakProfile { + profiles.m["goroutineleak"] = goroutineLeakProfile + } } } @@ -275,6 +287,7 @@ func Profiles() []*Profile { all := make([]*Profile, 0, len(profiles.m)) for _, p := range profiles.m { + all = append(all, p) } @@ -747,6 +760,23 @@ func writeGoroutine(w io.Writer, debug int) error { return writeRuntimeProfile(w, debug, "goroutine", pprof_goroutineProfileWithLabels) } +// writeGoroutineLeak first invokes a GC cycle that performs goroutine leak detection. +// It then writes the goroutine profile, filtering for leaked goroutines. +func writeGoroutineLeak(w io.Writer, debug int) error { + // Run the GC with leak detection first so that leaked goroutines + // may transition to the leaked state. + runtime_goroutineLeakGC() + + // If the debug flag is set sufficiently high, just defer to writing goroutine stacks + // like in a regular goroutine profile. Include non-leaked goroutines, too. + if debug >= 2 { + return writeGoroutineStacks(w) + } + + // Otherwise, write the goroutine leak profile. + return writeRuntimeProfile(w, debug, "goroutineleak", pprof_goroutineLeakProfileWithLabels) +} + func writeGoroutineStacks(w io.Writer) error { // We don't know how big the buffer needs to be to collect // all the goroutines. Start with 1 MB and try a few times, doubling each time. @@ -969,6 +999,9 @@ func writeProfileInternal(w io.Writer, debug int, name string, runtimeProfile fu //go:linkname pprof_goroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels func pprof_goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) +//go:linkname pprof_goroutineLeakProfileWithLabels runtime.pprof_goroutineLeakProfileWithLabels +func pprof_goroutineLeakProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) + //go:linkname pprof_cyclesPerSecond runtime/pprof.runtime_cyclesPerSecond func pprof_cyclesPerSecond() int64 diff --git a/src/runtime/pprof/runtime.go b/src/runtime/pprof/runtime.go index 8d37c7d3add146..690cab81ab5a5a 100644 --- a/src/runtime/pprof/runtime.go +++ b/src/runtime/pprof/runtime.go @@ -29,6 +29,12 @@ func runtime_setProfLabel(labels unsafe.Pointer) // runtime_getProfLabel is defined in runtime/proflabel.go. func runtime_getProfLabel() unsafe.Pointer +// runtime_goroutineleakcount is defined in runtime/proc.go. +func runtime_goroutineleakcount() int + +// runtime_goroutineLeakGC is defined in runtime/mgc.go. +func runtime_goroutineLeakGC() + // SetGoroutineLabels sets the current goroutine's labels to match ctx. // A new goroutine inherits the labels of the goroutine that created it. // This is a lower-level API than [Do], which should be used instead when possible. diff --git a/src/runtime/preempt.go b/src/runtime/preempt.go index 22727df74eead2..5367f66213804b 100644 --- a/src/runtime/preempt.go +++ b/src/runtime/preempt.go @@ -160,7 +160,7 @@ func suspendG(gp *g) suspendGState { s = _Gwaiting fallthrough - case _Grunnable, _Gsyscall, _Gwaiting: + case _Grunnable, _Gsyscall, _Gwaiting, _Gleaked: // Claim goroutine by setting scan bit. // This may race with execution or readying of gp. // The scan bit keeps it from transition state. @@ -269,6 +269,7 @@ func resumeG(state suspendGState) { case _Grunnable | _Gscan, _Gwaiting | _Gscan, + _Gleaked | _Gscan, _Gsyscall | _Gscan: casfrom_Gscanstatus(gp, s, s&^_Gscan) } diff --git a/src/runtime/proc.go b/src/runtime/proc.go index 4b1ab1af6a5075..e5686705293e8a 100644 --- a/src/runtime/proc.go +++ b/src/runtime/proc.go @@ -517,7 +517,7 @@ func acquireSudog() *sudog { s := pp.sudogcache[n-1] pp.sudogcache[n-1] = nil pp.sudogcache = pp.sudogcache[:n-1] - if s.elem != nil { + if s.elem.get() != nil { throw("acquireSudog: found s.elem != nil in cache") } releasem(mp) @@ -526,7 +526,7 @@ func acquireSudog() *sudog { //go:nosplit func releaseSudog(s *sudog) { - if s.elem != nil { + if s.elem.get() != nil { throw("runtime: sudog with non-nil elem") } if s.isSelect { @@ -541,7 +541,7 @@ func releaseSudog(s *sudog) { if s.waitlink != nil { throw("runtime: sudog with non-nil waitlink") } - if s.c != nil { + if s.c.get() != nil { throw("runtime: sudog with non-nil c") } gp := getg() @@ -1222,6 +1222,7 @@ func casfrom_Gscanstatus(gp *g, oldval, newval uint32) { _Gscanwaiting, _Gscanrunning, _Gscansyscall, + _Gscanleaked, _Gscanpreempted: if newval == oldval&^_Gscan { success = gp.atomicstatus.CompareAndSwap(oldval, newval) @@ -1242,6 +1243,7 @@ func castogscanstatus(gp *g, oldval, newval uint32) bool { case _Grunnable, _Grunning, _Gwaiting, + _Gleaked, _Gsyscall: if newval == oldval|_Gscan { r := gp.atomicstatus.CompareAndSwap(oldval, newval) @@ -5565,6 +5567,14 @@ func gcount(includeSys bool) int32 { return n } +// goroutineleakcount returns the number of leaked goroutines last reported by +// the runtime. +// +//go:linkname goroutineleakcount runtime/pprof.runtime_goroutineleakcount +func goroutineleakcount() int { + return work.goroutineLeak.count +} + func mcount() int32 { return int32(sched.mnext - sched.nmfreed) } diff --git a/src/runtime/runtime2.go b/src/runtime/runtime2.go index 042c3137cd03df..6016c6fde054fb 100644 --- a/src/runtime/runtime2.go +++ b/src/runtime/runtime2.go @@ -87,6 +87,9 @@ const ( // ready()ing this G. _Gpreempted // 9 + // _Gleaked represents a leaked goroutine caught by the GC. + _Gleaked // 10 + // _Gscan combined with one of the above states other than // _Grunning indicates that GC is scanning the stack. The // goroutine is not executing user code and the stack is owned @@ -104,6 +107,7 @@ const ( _Gscansyscall = _Gscan + _Gsyscall // 0x1003 _Gscanwaiting = _Gscan + _Gwaiting // 0x1004 _Gscanpreempted = _Gscan + _Gpreempted // 0x1009 + _Gscanleaked = _Gscan + _Gleaked // 0x100a ) const ( @@ -315,6 +319,78 @@ type gobuf struct { bp uintptr // for framepointer-enabled architectures } +// maybeTraceablePtr is a special pointer that is conditionally trackable +// by the GC. It consists of an address as a uintptr (vu) and a pointer +// to a data element (vp). +// +// maybeTraceablePtr values can be in one of three states: +// 1. Unset: vu == 0 && vp == nil +// 2. Untracked: vu != 0 && vp == nil +// 3. Tracked: vu != 0 && vp != nil +// +// Do not set fields manually. Use methods instead. +// Extend this type with additional methods if needed. +type maybeTraceablePtr struct { + vp unsafe.Pointer // For liveness only. + vu uintptr // Source of truth. +} + +// untrack unsets the pointer but preserves the address. +// This is used to hide the pointer from the GC. +// +//go:nosplit +func (p *maybeTraceablePtr) setUntraceable() { + p.vp = nil +} + +// setTraceable resets the pointer to the stored address. +// This is used to make the pointer visible to the GC. +// +//go:nosplit +func (p *maybeTraceablePtr) setTraceable() { + p.vp = unsafe.Pointer(p.vu) +} + +// set sets the pointer to the data element and updates the address. +// +//go:nosplit +func (p *maybeTraceablePtr) set(v unsafe.Pointer) { + p.vp = v + p.vu = uintptr(v) +} + +// get retrieves the pointer to the data element. +// +//go:nosplit +func (p *maybeTraceablePtr) get() unsafe.Pointer { + return unsafe.Pointer(p.vu) +} + +// uintptr returns the uintptr address of the pointer. +// +//go:nosplit +func (p *maybeTraceablePtr) uintptr() uintptr { + return p.vu +} + +// maybeTraceableChan extends conditionally trackable pointers (maybeTraceablePtr) +// to track hchan pointers. +// +// Do not set fields manually. Use methods instead. +type maybeTraceableChan struct { + maybeTraceablePtr +} + +//go:nosplit +func (p *maybeTraceableChan) set(c *hchan) { + p.maybeTraceablePtr.set(unsafe.Pointer(c)) +} + +//go:nosplit +func (p *maybeTraceableChan) get() *hchan { + return (*hchan)(p.maybeTraceablePtr.get()) +} + // sudog (pseudo-g) represents a g in a wait list, such as for sending/receiving // on a channel. // @@ -334,7 +410,8 @@ type sudog struct { next *sudog prev *sudog - elem unsafe.Pointer // data element (may point to stack) + + elem maybeTraceablePtr // data element (may point to stack) // The following fields are never accessed concurrently. // For channels, waitlink is only accessed by g. @@ -362,10 +439,10 @@ type sudog struct { // in the second entry in the list.) waiters uint16 - parent *sudog // semaRoot binary tree - waitlink *sudog // g.waiting list or semaRoot - waittail *sudog // semaRoot - c *hchan // channel + parent *sudog // semaRoot binary tree + waitlink *sudog // g.waiting list or semaRoot + waittail *sudog // semaRoot + c maybeTraceableChan // channel } type libcall struct { @@ -1072,24 +1149,24 @@ const ( waitReasonZero waitReason = iota // "" waitReasonGCAssistMarking // "GC assist marking" waitReasonIOWait // "IO wait" - waitReasonChanReceiveNilChan // "chan receive (nil chan)" - waitReasonChanSendNilChan // "chan send (nil chan)" waitReasonDumpingHeap // "dumping heap" waitReasonGarbageCollection // "garbage collection" waitReasonGarbageCollectionScan // "garbage collection scan" waitReasonPanicWait // "panicwait" - waitReasonSelect // "select" - waitReasonSelectNoCases // "select (no cases)" waitReasonGCAssistWait // "GC assist wait" waitReasonGCSweepWait // "GC sweep wait" waitReasonGCScavengeWait // "GC scavenge wait" - waitReasonChanReceive // "chan receive" - waitReasonChanSend // "chan send" waitReasonFinalizerWait // "finalizer wait" waitReasonForceGCIdle // "force gc (idle)" waitReasonUpdateGOMAXPROCSIdle // "GOMAXPROCS updater (idle)" waitReasonSemacquire // "semacquire" waitReasonSleep // "sleep" + waitReasonChanReceiveNilChan // "chan receive (nil chan)" + waitReasonChanSendNilChan // "chan send (nil chan)" + waitReasonSelectNoCases // "select (no cases)" + waitReasonSelect // "select" + waitReasonChanReceive // "chan receive" + waitReasonChanSend // "chan send" waitReasonSyncCondWait // "sync.Cond.Wait" waitReasonSyncMutexLock // "sync.Mutex.Lock" waitReasonSyncRWMutexRLock // "sync.RWMutex.RLock" @@ -1175,12 +1252,34 @@ func (w waitReason) String() string { return waitReasonStrings[w] } +// isMutexWait returns true if the goroutine is blocked because of +// sync.Mutex.Lock or sync.RWMutex.[R]Lock. +// +//go:nosplit func (w waitReason) isMutexWait() bool { return w == waitReasonSyncMutexLock || w == waitReasonSyncRWMutexRLock || w == waitReasonSyncRWMutexLock } +// isSyncWait returns true if the goroutine is blocked because of +// sync library primitive operations. +// +//go:nosplit +func (w waitReason) isSyncWait() bool { + return waitReasonSyncCondWait <= w && w <= waitReasonSyncWaitGroupWait +} + +// isChanWait is true if the goroutine is blocked because of non-nil +// channel operations or a select statement with at least one case. +// +//go:nosplit +func (w waitReason) isChanWait() bool { + return w == waitReasonSelect || + w == waitReasonChanReceive || + w == waitReasonChanSend +} + func (w waitReason) isWaitingForSuspendG() bool { return isWaitingForSuspendG[w] } diff --git a/src/runtime/select.go b/src/runtime/select.go index 113dc8ad19e984..553f6960eb1286 100644 --- a/src/runtime/select.go +++ b/src/runtime/select.go @@ -83,7 +83,7 @@ func selparkcommit(gp *g, _ unsafe.Pointer) bool { // channels in lock order. var lastc *hchan for sg := gp.waiting; sg != nil; sg = sg.waitlink { - if sg.c != lastc && lastc != nil { + if sg.c.get() != lastc && lastc != nil { // As soon as we unlock the channel, fields in // any sudog with that channel may change, // including c and waitlink. Since multiple @@ -92,7 +92,7 @@ func selparkcommit(gp *g, _ unsafe.Pointer) bool { // of a channel. unlock(&lastc.lock) } - lastc = sg.c + lastc = sg.c.get() } if lastc != nil { unlock(&lastc.lock) @@ -320,12 +320,12 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo sg.isSelect = true // No stack splits between assigning elem and enqueuing // sg on gp.waiting where copystack can find it. - sg.elem = cas.elem + sg.elem.set(cas.elem) sg.releasetime = 0 if t0 != 0 { sg.releasetime = -1 } - sg.c = c + sg.c.set(c) // Construct waiting list in lock order. *nextp = sg nextp = &sg.waitlink @@ -368,8 +368,8 @@ func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, blo // Clear all elem before unlinking from gp.waiting. for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink { sg1.isSelect = false - sg1.elem = nil - sg1.c = nil + sg1.elem.set(nil) + sg1.c.set(nil) } gp.waiting = nil diff --git a/src/runtime/sema.go b/src/runtime/sema.go index 6af49b1b0c42d9..0fe0f2a2c20739 100644 --- a/src/runtime/sema.go +++ b/src/runtime/sema.go @@ -303,7 +303,10 @@ func cansemacquire(addr *uint32) bool { // queue adds s to the blocked goroutines in semaRoot. func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { s.g = getg() - s.elem = unsafe.Pointer(addr) + s.elem.set(unsafe.Pointer(addr)) + // Storing this pointer so that we can trace the semaphore address + // from the blocked goroutine when checking for goroutine leaks. + s.g.waiting = s s.next = nil s.prev = nil s.waiters = 0 @@ -311,7 +314,7 @@ func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { var last *sudog pt := &root.treap for t := *pt; t != nil; t = *pt { - if t.elem == unsafe.Pointer(addr) { + if uintptr(unsafe.Pointer(addr)) == t.elem.uintptr() { // Already have addr in list. if lifo { // Substitute s in t's place in treap. @@ -357,7 +360,7 @@ func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { return } last = t - if uintptr(unsafe.Pointer(addr)) < uintptr(t.elem) { + if uintptr(unsafe.Pointer(addr)) < t.elem.uintptr() { pt = &t.prev } else { pt = &t.next @@ -402,11 +405,13 @@ func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) { func (root *semaRoot) dequeue(addr *uint32) (found *sudog, now, tailtime int64) { ps := &root.treap s := *ps + for ; s != nil; s = *ps { - if s.elem == unsafe.Pointer(addr) { + if uintptr(unsafe.Pointer(addr)) == s.elem.uintptr() { goto Found } - if uintptr(unsafe.Pointer(addr)) < uintptr(s.elem) { + + if uintptr(unsafe.Pointer(addr)) < s.elem.uintptr() { ps = &s.prev } else { ps = &s.next @@ -470,8 +475,10 @@ Found: } tailtime = s.acquiretime } + // Goroutine is no longer blocked. Clear the waiting pointer. + s.g.waiting = nil s.parent = nil - s.elem = nil + s.elem.set(nil) s.next = nil s.prev = nil s.ticket = 0 @@ -590,6 +597,10 @@ func notifyListWait(l *notifyList, t uint32) { // Enqueue itself. s := acquireSudog() s.g = getg() + // Storing this pointer so that we can trace the condvar address + // from the blocked goroutine when checking for goroutine leaks. + s.elem.set(unsafe.Pointer(l)) + s.g.waiting = s s.ticket = t s.releasetime = 0 t0 := int64(0) @@ -607,6 +618,10 @@ func notifyListWait(l *notifyList, t uint32) { if t0 != 0 { blockevent(s.releasetime-t0, 2) } + // Goroutine is no longer blocked. Clear up its waiting pointer, + // and clean up the sudog before releasing it. + s.g.waiting = nil + s.elem.set(nil) releaseSudog(s) } diff --git a/src/runtime/sizeof_test.go b/src/runtime/sizeof_test.go index de859866a5adb2..5888177f0ea7a1 100644 --- a/src/runtime/sizeof_test.go +++ b/src/runtime/sizeof_test.go @@ -22,7 +22,7 @@ func TestSizeof(t *testing.T) { _64bit uintptr // size on 64bit platforms }{ {runtime.G{}, 280 + xreg, 440 + xreg}, // g, but exported for testing - {runtime.Sudog{}, 56, 88}, // sudog, but exported for testing + {runtime.Sudog{}, 64, 104}, // sudog, but exported for testing } if xreg > runtime.PtrSize { diff --git a/src/runtime/stack.go b/src/runtime/stack.go index d809820b076dfd..55e97e77afa957 100644 --- a/src/runtime/stack.go +++ b/src/runtime/stack.go @@ -821,7 +821,8 @@ func adjustsudogs(gp *g, adjinfo *adjustinfo) { // the data elements pointed to by a SudoG structure // might be in the stack. for s := gp.waiting; s != nil; s = s.waitlink { - adjustpointer(adjinfo, unsafe.Pointer(&s.elem)) + adjustpointer(adjinfo, unsafe.Pointer(&s.elem.vu)) + adjustpointer(adjinfo, unsafe.Pointer(&s.elem.vp)) } } @@ -834,7 +835,7 @@ func fillstack(stk stack, b byte) { func findsghi(gp *g, stk stack) uintptr { var sghi uintptr for sg := gp.waiting; sg != nil; sg = sg.waitlink { - p := uintptr(sg.elem) + uintptr(sg.c.elemsize) + p := sg.elem.uintptr() + uintptr(sg.c.get().elemsize) if stk.lo <= p && p < stk.hi && p > sghi { sghi = p } @@ -853,7 +854,7 @@ func syncadjustsudogs(gp *g, used uintptr, adjinfo *adjustinfo) uintptr { // Lock channels to prevent concurrent send/receive. var lastc *hchan for sg := gp.waiting; sg != nil; sg = sg.waitlink { - if sg.c != lastc { + if sg.c.get() != lastc { // There is a ranking cycle here between gscan bit and // hchan locks. Normally, we only allow acquiring hchan // locks and then getting a gscan bit. In this case, we @@ -863,9 +864,9 @@ func syncadjustsudogs(gp *g, used uintptr, adjinfo *adjustinfo) uintptr { // suspended. So, we get a special hchan lock rank here // that is lower than gscan, but doesn't allow acquiring // any other locks other than hchan. - lockWithRank(&sg.c.lock, lockRankHchanLeaf) + lockWithRank(&sg.c.get().lock, lockRankHchanLeaf) } - lastc = sg.c + lastc = sg.c.get() } // Adjust sudogs. @@ -885,10 +886,10 @@ func syncadjustsudogs(gp *g, used uintptr, adjinfo *adjustinfo) uintptr { // Unlock channels. lastc = nil for sg := gp.waiting; sg != nil; sg = sg.waitlink { - if sg.c != lastc { - unlock(&sg.c.lock) + if sg.c.get() != lastc { + unlock(&sg.c.get().lock) } - lastc = sg.c + lastc = sg.c.get() } return sgsize diff --git a/src/runtime/testdata/testgoroutineleakprofile/commonpatterns.go b/src/runtime/testdata/testgoroutineleakprofile/commonpatterns.go new file mode 100644 index 00000000000000..353e48ee7034f7 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/commonpatterns.go @@ -0,0 +1,277 @@ +// 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 + +import ( + "context" + "fmt" + "os" + "runtime" + "runtime/pprof" + "time" +) + +// Common goroutine leak patterns. Extracted from: +// "Unveiling and Vanquishing Goroutine Leaks in Enterprise Microservices: A Dynamic Analysis Approach" +// doi:10.1109/CGO57630.2024.10444835 +// +// Tests in this file are not flaky iff. the test is run with GOMAXPROCS=1. +// The main goroutine forcefully yields via `runtime.Gosched()` before +// running the profiler. This moves them to the back of the run queue, +// allowing the leaky goroutines to be scheduled beforehand and get stuck. + +func init() { + register("NoCloseRange", NoCloseRange) + register("MethodContractViolation", MethodContractViolation) + register("DoubleSend", DoubleSend) + register("EarlyReturn", EarlyReturn) + register("NCastLeak", NCastLeak) + register("Timeout", Timeout) +} + +// Incoming list of items and the number of workers. +func noCloseRange(list []any, workers int) { + ch := make(chan any) + + // Create each worker + for i := 0; i < workers; i++ { + go func() { + + // Each worker waits for an item and processes it. + for item := range ch { + // Process each item + _ = item + } + }() + } + + // Send each item to one of the workers. + for _, item := range list { + // Sending can leak if workers == 0 or if one of the workers panics + ch <- item + } + // The channel is never closed, so workers leak once there are no more + // items left to process. +} + +func NoCloseRange() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + go noCloseRange([]any{1, 2, 3}, 0) + go noCloseRange([]any{1, 2, 3}, 3) +} + +// A worker processes items pushed to `ch` one by one in the background. +// When the worker is no longer needed, it must be closed with `Stop`. +// +// Specifications: +// +// A worker may be started any number of times, but must be stopped only once. +// Stopping a worker multiple times will lead to a close panic. +// Any worker that is started must eventually be stopped. +// Failing to stop a worker results in a goroutine leak +type worker struct { + ch chan any + done chan any +} + +// Start spawns a background goroutine that extracts items pushed to the queue. +func (w worker) Start() { + go func() { + + for { + select { + case <-w.ch: // Normal workflow + case <-w.done: + return // Shut down + } + } + }() +} + +func (w worker) Stop() { + // Allows goroutine created by Start to terminate + close(w.done) +} + +func (w worker) AddToQueue(item any) { + w.ch <- item +} + +// worker limited in scope by workerLifecycle +func workerLifecycle(items []any) { + // Create a new worker + w := worker{ + ch: make(chan any), + done: make(chan any), + } + // Start worker + w.Start() + + // Operate on worker + for _, item := range items { + w.AddToQueue(item) + } + + runtime.Gosched() + // Exits without calling ’Stop’. Goroutine created by `Start` eventually leaks. +} + +func MethodContractViolation() { + prof := pprof.Lookup("goroutineleak") + defer func() { + runtime.Gosched() + prof.WriteTo(os.Stdout, 2) + }() + + workerLifecycle(make([]any, 10)) + runtime.Gosched() +} + +// doubleSend incoming channel must send a message (incoming error simulates an error generated internally). +func doubleSend(ch chan any, err error) { + if err != nil { + // In case of an error, send nil. + ch <- nil + // Return is missing here. + } + // Otherwise, continue with normal behaviour + // This send is still executed in the error case, which may lead to a goroutine leak. + ch <- struct{}{} +} + +func DoubleSend() { + prof := pprof.Lookup("goroutineleak") + ch := make(chan any) + defer func() { + runtime.Gosched() + prof.WriteTo(os.Stdout, 2) + }() + + go func() { + doubleSend(ch, nil) + }() + <-ch + + go func() { + doubleSend(ch, fmt.Errorf("error")) + }() + <-ch + + ch1 := make(chan any, 1) + go func() { + doubleSend(ch1, fmt.Errorf("error")) + }() + <-ch1 +} + +// earlyReturn demonstrates a common pattern of goroutine leaks. +// A return statement interrupts the evaluation of the parent goroutine before it can consume a message. +// Incoming error simulates an error produced internally. +func earlyReturn(err error) { + // Create a synchronous channel + ch := make(chan any) + + go func() { + + // Send something to the channel. + // Leaks if the parent goroutine terminates early. + ch <- struct{}{} + }() + + if err != nil { + // Interrupt evaluation of parent early in case of error. + // Sender leaks. + return + } + + // Only receive if there is no error. + <-ch +} + +func EarlyReturn() { + prof := pprof.Lookup("goroutineleak") + defer func() { + runtime.Gosched() + prof.WriteTo(os.Stdout, 2) + }() + + go earlyReturn(fmt.Errorf("error")) +} + +// nCastLeak processes a number of items. First result to pass the post is retrieved from the channel queue. +func nCastLeak(items []any) { + // Channel is synchronous. + ch := make(chan any) + + // Iterate over every item + for range items { + go func() { + + // Process item and send result to channel + ch <- struct{}{} + // Channel is synchronous: only one sender will synchronise + }() + } + // Retrieve first result. All other senders block. + // Receiver blocks if there are no senders. + <-ch +} + +func NCastLeak() { + prof := pprof.Lookup("goroutineleak") + defer func() { + for i := 0; i < yieldCount; i++ { + // Yield enough times to allow all the leaky goroutines to + // reach the execution point. + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + go func() { + nCastLeak(nil) + }() + + go func() { + nCastLeak(make([]any, 5)) + }() +} + +// A context is provided to short-circuit evaluation, leading +// the sender goroutine to leak. +func timeout(ctx context.Context) { + ch := make(chan any) + + go func() { + ch <- struct{}{} + }() + + select { + case <-ch: // Receive message + // Sender is released + case <-ctx.Done(): // Context was cancelled or timed out + // Sender is leaked + } +} + +func Timeout() { + prof := pprof.Lookup("goroutineleak") + defer func() { + runtime.Gosched() + prof.WriteTo(os.Stdout, 2) + }() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + for i := 0; i < 100; i++ { + go timeout(ctx) + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/LICENSE b/src/runtime/testdata/testgoroutineleakprofile/goker/LICENSE new file mode 100644 index 00000000000000..f4b4b8abc4b372 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) +Copyright © 2021 Institute of Computing Technology, University of New South Wales + + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/README.md b/src/runtime/testdata/testgoroutineleakprofile/goker/README.md new file mode 100644 index 00000000000000..88c50e1e480a08 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/README.md @@ -0,0 +1,1847 @@ +# GoKer + +The following examples are obtained from the publication +"GoBench: A Benchmark Suite of Real-World Go Concurrency Bugs" +(doi:10.1109/CGO51591.2021.9370317). + +**Authors** +Ting Yuan (yuanting@ict.ac.cn): + State Key Laboratory of Computer Architecture, Institute of Computing Technology, Chinese Academy of Sciences, + University of Chinese Academy of Sciences, Beijing, China; +Guangwei Li (liguangwei@ict.ac.cn): + State Key Laboratory of Computer Architecture, Institute of Computing Technology, Chinese Academy of Sciences, + University of Chinese Academy of Sciences, Beijing, China; +Jie Lu† (lujie@ict.ac.an): + State Key Laboratory of Computer Architecture, Institute of Computing Technology, Chinese Academy of Sciences; +Chen Liu (liuchen17z@ict.ac.cn): + State Key Laboratory of Computer Architecture, Institute of Computing Technology, Chinese Academy of Sciences, + University of Chinese Academy of Sciences, Beijing, China +Lian Li (lianli@ict.ac.cn): + State Key Laboratory of Computer Architecture, Institute of Computing Technology, Chinese Academy of Sciences, + University of Chinese Academy of Sciences, Beijing, China; +Jingling Xue (jingling@cse.unsw.edu.au): + University of New South Wales, School of Computer Science and Engineering, Sydney, Australia + +White paper: https://lujie.ac.cn/files/papers/GoBench.pdf + +The examples have been modified in order to run the goroutine leak +profiler. Buggy snippets are moved from within a unit test to separate +applications. Each is then independently executed, possibly as multiple +copies within the same application in order to exercise more interleavings. +Concurrently, the main program sets up a waiting period (typically 1ms), followed +by a goroutine leak profile request. Other modifications may involve injecting calls +to `runtime.Gosched()`, to more reliably exercise buggy interleavings, or reductions +in waiting periods when calling `time.Sleep`, in order to reduce overall testing time. + +The resulting goroutine leak profile is analyzed to ensure that no unexpected leaks occurred, +and that the expected leaks did occur. If the leak is flaky, the only purpose of the expected +leak list is to protect against unexpected leaks. + +The entries below document each of the corresponding leaks. + +## Cockroach/10214 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#10214]|[pull request]|[patch]| Resource | AB-BA leak | + +[cockroach#10214]:(cockroach10214_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/10214/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/10214 + +### Description + +This goroutine leak is caused by different order when acquiring +coalescedMu.Lock() and raftMu.Lock(). The fix is to refactor sendQueuedHeartbeats() +so that cockroachdb can unlock coalescedMu before locking raftMu. + +### Example execution + +```go +G1 G2 +------------------------------------------------------------------------------------ +s.sendQueuedHeartbeats() . +s.coalescedMu.Lock() [L1] . +s.sendQueuedHeartbeatsToNode() . +s.mu.replicas[0].reportUnreachable() . +s.mu.replicas[0].raftMu.Lock() [L2] . +. s.mu.replicas[0].tick() +. s.mu.replicas[0].raftMu.Lock() [L2] +. s.mu.replicas[0].tickRaftMuLocked() +. s.mu.replicas[0].mu.Lock() [L3] +. s.mu.replicas[0].maybeQuiesceLocked() +. s.mu.replicas[0].maybeCoalesceHeartbeat() +. s.coalescedMu.Lock() [L1] +--------------------------------G1,G2 leak------------------------------------------ +``` + +## Cockroach/1055 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#1055]|[pull request]|[patch]| Mixed | Channel & WaitGroup | + +[cockroach#1055]:(cockroach1055_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/1055/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/1055 + +### Description + +1. `Stop()` is called and blocked at `s.stop.Wait()` after acquiring the lock. +2. `StartTask()` is called and attempts to acquire the lock. It is then blocked. +3. `Stop()` never finishes since the task doesn't call SetStopped. + +### Example execution + +```go +G1 G2.0 G2.1 G2.2 G3 +------------------------------------------------------------------------------------------------------------------------------- +s[0].stop.Add(1) [1] +go func() [G2.0] +s[1].stop.Add(1) [1] . +go func() [G2.1] . +s[2].stop.Add(1) [1] . . +go func() [G2.2] . . +go func() [G3] . . . +<-done . . . . +. s[0].StartTask() . . . +. s[0].draining == 0 . . . +. . s[1].StartTask() . . +. . s[1].draining == 0 . . +. . . s[2].StartTask() . +. . . s[2].draining == 0 . +. . . . s[0].Quiesce() +. . . . s[0].mu.Lock() [L1[0]] +. s[0].mu.Lock() [L1[0]] . . . +. s[0].drain.Add(1) [1] . . . +. s[0].mu.Unlock() [L1[0]] . . . +. <-s[0].ShouldStop() . . . +. . . . s[0].draining = 1 +. . . . s[0].drain.Wait() +. . s[0].mu.Lock() [L1[1]] . . +. . s[1].drain.Add(1) [1] . . +. . s[1].mu.Unlock() [L1[1]] . . +. . <-s[1].ShouldStop() . . +. . . s[2].mu.Lock() [L1[2]] . +. . . s[2].drain.Add() [1] . +. . . s[2].mu.Unlock() [L1[2]] . +. . . <-s[2].ShouldStop() . +----------------------------------------------------G1, G2.[0..2], G3 leak----------------------------------------------------- +``` + +## Cockroach/10790 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#10790]|[pull request]|[patch]| Communication | Channel & Context | + +[cockroach#10790]:(cockroach10790_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/10790/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/10790 + +### Description + +It is possible that a message from `ctxDone` will make `beginCmds` +return without draining the channel `ch`, so that anonymous function +goroutines will leak. + +### Example execution + +```go +G1 G2 helper goroutine +----------------------------------------------------- +. . r.sendChans() +r.beginCmds() . . +. . ch1 <- true +<- ch1 . . +. . ch2 <- true +... +. cancel() +<- ch1 +------------------G1 leak---------------------------- +``` + +## Cockroach/13197 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#13197]|[pull request]|[patch]| Communication | Channel & Context | + +[cockroach#13197]:(cockroach13197_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/13197/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/13197 + +### Description + +One goroutine executing `(*Tx).awaitDone()` blocks and +waiting for a signal `context.Done()`. + +### Example execution + +```go +G1 G2 +------------------------------- +begin() +. awaitDone() +return . +. <-tx.ctx.Done() +-----------G2 leaks------------ +``` + +## Cockroach/13755 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#13755]|[pull request]|[patch]| Communication | Channel & Context | + +[cockroach#13755]:(cockroach13755_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/13755/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/13755 + +### Description + +The buggy code does not close the db query result (rows), +so that one goroutine running `(*Rows).awaitDone` is blocked forever. +The blocking goroutine is waiting for cancel signal from context. + +### Example execution + +```go +G1 G2 +--------------------------------------- +initContextClose() +. awaitDone() +return . +. <-tx.ctx.Done() +---------------G2 leaks---------------- +``` + +## Cockroach/1462 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#1462]|[pull request]|[patch]| Mixed | Channel & WaitGroup | + +[cockroach#1462]:(cockroach1462_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/1462/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/1462 + +### Description + +Executing `<-stopper.ShouldStop()` in `processEventsUntil` may cause +goroutines created by `lt.RunWorker` in `lt.start` to be stuck sending +a message over `lt.Events`. The main thread is then stuck at `s.stop.Wait()`, +since the sender goroutines cannot call `s.stop.Done()`. + +### Example execution + +```go +G1 G2 G3 +------------------------------------------------------------------------------------------------------- +NewLocalInterceptableTransport() +lt.start() +lt.stopper.RunWorker() +s.AddWorker() +s.stop.Add(1) [1] +go func() [G2] +stopper.RunWorker() . +s.AddWorker() . +s.stop.Add(1) [2] . +go func() [G3] . +s.Stop() . . +s.Quiesce() . . +. select [default] . +. lt.Events <- interceptMessage(0) . +close(s.stopper) . . +. . select [<-stopper.ShouldStop()] +. . <<>> +s.stop.Wait() . +----------------------------------------------G1,G2 leak----------------------------------------------- +``` + +## Cockroach/16167 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#16167]|[pull request]|[patch]| Resource | Double Locking | + +[cockroach#16167]:(cockroach16167_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/16167/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/16167 + +### Description + +This is another example of goroutine leaks caused by recursively +acquiring `RWLock`. +There are two lock variables (`systemConfigCond` and `systemConfigMu`) +which refer to the same underlying lock. The leak invovlves two goroutines. +The first acquires `systemConfigMu.Lock()`, then tries to acquire `systemConfigMu.RLock()`. +The second acquires `systemConfigMu.Lock()`. +If the second goroutine interleaves in between the two lock operations of the +first goroutine, both goroutines will leak. + +### Example execution + +```go +G1 G2 +--------------------------------------------------------------- +. e.Start() +. e.updateSystemConfig() +e.execParsed() . +e.systemConfigCond.L.Lock() [L1] . +. e.systemConfigMu.Lock() [L1] +e.systemConfigMu.RLock() [L1] . +------------------------G1,G2 leak----------------------------- +``` + +## Cockroach/18101 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#18101]|[pull request]|[patch]| Resource | Double Locking | + +[cockroach#18101]:(cockroach18101_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/18101/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/18101 + +### Description + +The `context.Done()` signal short-circuits the reader goroutine, but not +the senders, leading them to leak. + +### Example execution + +```go +G1 G2 helper goroutine +-------------------------------------------------------------- +restore() +. splitAndScatter() +<-readyForImportCh . +<-readyForImportCh <==> readyForImportCh<- +... +. . cancel() +<> . <> + readyForImportCh<- +-----------------------G2 leaks-------------------------------- +``` + +## Cockroach/2448 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#2448]|[pull request]|[patch]| Communication | Channel | + +[cockroach#2448]:(cockroach2448_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/2448/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/2448 + +### Description + +This bug is caused by two goroutines waiting for each other +to unblock their channels: + +1) `MultiRaft` sends the commit event for the Membership change +2) `store.processRaft` takes it and begins processing +3) another command commits and triggers another `sendEvent`, but + this blocks since `store.processRaft` isn't ready for another + `select`. Consequently the main `MultiRaft` loop is waiting for + that as well. +4) the `Membership` change was applied to the range, and the store + now tries to execute the callback +5) the callback tries to write to `callbackChan`, but that is + consumed by the `MultiRaft` loop, which is currently waiting + for `store.processRaft` to consume from the events channel, + which it will only do after the callback has completed. + +### Example execution + +```go +G1 G2 +-------------------------------------------------------------------------- +s.processRaft() st.start() +select . +. select [default] +. s.handleWriteResponse() +. s.sendEvent() +. select +<-s.multiraft.Events <----> m.Events <- event +. select [default] +. s.handleWriteResponse() +. s.sendEvent() +. select [m.Events<-, <-s.stopper.ShouldStop()] +callback() . +select [ + m.callbackChan<-, + <-s.stopper.ShouldStop() +] . +------------------------------G1,G2 leak---------------------------------- +``` + +## Cockroach/24808 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#24808]|[pull request]|[patch]| Communication | Channel | + +[cockroach#24808]:(cockroach24808_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/24808/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/24808 + +### Description + +When we `Start` the `Compactor`, it may already have received +`Suggestions`, leaking the previously blocking write to a full channel. + +### Example execution + +```go +G1 +------------------------------------------------ +... +compactor.ch <- +compactor.Start() +compactor.ch <- +--------------------G1 leaks-------------------- +``` + +## Cockroach/25456 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#25456]|[pull request]|[patch]| Communication | Channel | + +[cockroach#25456]:(cockroach25456_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/25456/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/25456 + +### Description + +When `CheckConsistency` (in the complete code) returns an error, the queue +checks whether the store is draining to decide whether the error is worth +logging. This check was incorrect and would block until the store actually +started draining. + +### Example execution + +```go +G1 +--------------------------------------- +... +<-repl.store.Stopper().ShouldQuiesce() +---------------G1 leaks---------------- +``` + +## Cockroach/35073 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#35073]|[pull request]|[patch]| Communication | Channel | + +[cockroach#35073]:(cockroach35073_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/35073/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/35073 + +### Description + +Previously, the outbox could fail during startup without closing its +`RowChannel`. This could lead to goroutine leaks in rare cases due +to channel communication mismatch. + +### Example execution + +## Cockroach/35931 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#35931]|[pull request]|[patch]| Communication | Channel | + +[cockroach#35931]:(cockroach35931_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/35931/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/35931 + +### Description + +Previously, if a processor that reads from multiple inputs was waiting +on one input to provide more data, and the other input was full, and +both inputs were connected to inbound streams, it was possible to +cause goroutine leaks during flow cancellation when trying to propagate +the cancellation metadata messages into the flow. The cancellation method +wrote metadata messages to each inbound stream one at a time, so if the +first one was full, the canceller would block and never send a cancellation +message to the second stream, which was the one actually being read from. + +### Example execution + +## Cockroach/3710 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#3710]|[pull request]|[patch]| Resource | RWR Deadlock | + +[cockroach#3710]:(cockroach3710_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/3710/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/3710 + +### Description + +The goroutine leak is caused by acquiring a RLock twice in a call chain. +`ForceRaftLogScanAndProcess(acquire s.mu.RLock())` +`-> MaybeAdd()` +`-> shouldQueue()` +`-> getTruncatableIndexes()` +`->RaftStatus(acquire s.mu.Rlock())` + +### Example execution + +```go +G1 G2 +------------------------------------------------------------ +store.ForceRaftLogScanAndProcess() +s.mu.RLock() +s.raftLogQueue.MaybeAdd() +bq.impl.shouldQueue() +getTruncatableIndexes() +r.store.RaftStatus() +. store.processRaft() +. s.mu.Lock() +s.mu.RLock() +----------------------G1,G2 leak----------------------------- +``` + +## Cockroach/584 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#584]|[pull request]|[patch]| Resource | Double Locking | + +[cockroach#584]:(cockroach584_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/584/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/584 + +### Description + +Missing call to `mu.Unlock()` before the `break` in the loop. + +### Example execution + +```go +G1 +--------------------------- +g.bootstrap() +g.mu.Lock() [L1] +if g.closed { ==> break +g.manage() +g.mu.Lock() [L1] +----------G1 leaks--------- +``` + +## Cockroach/6181 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#6181]|[pull request]|[patch]| Resource | RWR Deadlock | + +[cockroach#6181]:(cockroach6181_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/6181/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/6181 + +### Description + +The same `RWMutex` may be recursively acquired for both reading and writing. + +### Example execution + +```go +G1 G2 G3 ... +----------------------------------------------------------------------------------------------- +testRangeCacheCoalescedRquests() +initTestDescriptorDB() +pauseLookupResumeAndAssert() +return +. doLookupWithToken() +. . doLookupWithToken() +. rc.LookupRangeDescriptor() . +. . rc.LookupRangeDescriptor() +. rdc.rangeCacheMu.RLock() . +. rdc.String() . +. . rdc.rangeCacheMu.RLock() +. . fmt.Printf() +. . rdc.rangeCacheMu.RUnlock() +. . rdc.rangeCacheMu.Lock() +. rdc.rangeCacheMu.RLock() . +-----------------------------------G2,G3,... leak---------------------------------------------- +``` + +## Cockroach/7504 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#7504]|[pull request]|[patch]| Resource | AB-BA Deadlock | + +[cockroach#7504]:(cockroach7504_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/7504/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/7504 + +### Description + +The locks are acquired as `leaseState` and `tableNameCache` in `Release()`, but +as `tableNameCache` and `leaseState` in `AcquireByName`, leading to an AB-BA deadlock. + +### Example execution + +```go +G1 G2 +----------------------------------------------------- +mgr.AcquireByName() mgr.Release() +m.tableNames.get(id) . +c.mu.Lock() [L2] . +. t.release(lease) +. t.mu.Lock() [L3] +. s.mu.Lock() [L1] +lease.mu.Lock() [L1] . +. t.removeLease(s) +. t.tableNameCache.remove() +. c.mu.Lock() [L2] +---------------------G1, G2 leak--------------------- +``` + +## Cockroach/9935 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[cockroach#9935]|[pull request]|[patch]| Resource | Double Locking | + +[cockroach#9935]:(cockroach9935_test.go) +[patch]:https://github.com/cockroachdb/cockroach/pull/9935/files +[pull request]:https://github.com/cockroachdb/cockroach/pull/9935 + +### Description + +This bug is caused by acquiring `l.mu.Lock()` twice. + +### Example execution + +## Etcd/10492 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#10492]|[pull request]|[patch]| Resource | Double locking | + +[etcd#10492]:(etcd10492_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/10492/files +[pull request]:https://github.com/etcd-io/etcd/pull/10492 + +### Description + +A simple double locking case for lines 19, 31. + +## Etcd/5509 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#5509]|[pull request]|[patch]| Resource | Double locking | + +[etcd#5509]:(etcd5509_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/5509/files +[pull request]:https://github.com/etcd-io/etcd/pull/5509 + +### Description + +`r.acquire()` returns holding `r.client.mu.RLock()` on a failure path (line 42). +This causes any call to `client.Close()` to leak goroutines. + +## Etcd/6708 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#6708]|[pull request]|[patch]| Resource | Double locking | + +[etcd#6708]:(etcd6708_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/6708/files +[pull request]:https://github.com/etcd-io/etcd/pull/6708 + +### Description + +Line 54, 49 double locking + +## Etcd/6857 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#6857]|[pull request]|[patch]| Communication | Channel | + +[etcd#6857]:(etcd6857_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/6857/files +[pull request]:https://github.com/etcd-io/etcd/pull/6857 + +### Description + +Choosing a different case in a `select` statement (`n.stop`) will +lead to goroutine leaks when sending over `n.status`. + +### Example execution + +```go +G1 G2 G3 +------------------------------------------- +n.run() . . +. . n.Stop() +. . n.stop<- +<-n.stop . . +. . <-n.done +close(n.done) . . +return . . +. . return +. n.Status() +. n.status<- +----------------G2 leaks------------------- +``` + +## Etcd/6873 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#6873]|[pull request]|[patch]| Mixed | Channel & Lock | + +[etcd#6873]:(etcd6873_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/6873/files +[pull request]:https://github.com/etcd-io/etcd/pull/6873 + +### Description + +This goroutine leak involves a goroutine acquiring a lock and being +blocked over a channel operation with no partner, while another tries +to acquire the same lock. + +### Example execution + +```go +G1 G2 G3 +-------------------------------------------------------------- +newWatchBroadcasts() +wbs.update() +wbs.updatec <- +return +. <-wbs.updatec . +. wbs.coalesce() . +. . wbs.stop() +. . wbs.mu.Lock() +. . close(wbs.updatec) +. . <-wbs.donec +. wbs.mu.Lock() . +---------------------G2,G3 leak-------------------------------- +``` + +## Etcd/7492 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#7492]|[pull request]|[patch]| Mixed | Channel & Lock | + +[etcd#7492]:(etcd7492_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/7492/files +[pull request]:https://github.com/etcd-io/etcd/pull/7492 + +### Description + +This goroutine leak involves a goroutine acquiring a lock and being +blocked over a channel operation with no partner, while another tries +to acquire the same lock. + +### Example execution + +```go +G2 G1 +--------------------------------------------------------------- +. stk.run() +ts.assignSimpleTokenToUser() . +t.simpleTokensMu.Lock() . +t.simpleTokenKeeper.addSimpleToken() . +tm.addSimpleTokenCh <- true . +. <-tm.addSimpleTokenCh +t.simpleTokensMu.Unlock() . +ts.assignSimpleTokenToUser() . +... +t.simpleTokensMu.Lock() +. <-tokenTicker.C +tm.addSimpleTokenCh <- true . +. tm.deleteTokenFunc() +. t.simpleTokensMu.Lock() +---------------------------G1,G2 leak-------------------------- +``` + +## Etcd/7902 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[etcd#7902]|[pull request]|[patch]| Mixed | Channel & Lock | + +[etcd#7902]:(etcd7902_test.go) +[patch]:https://github.com/etcd-io/etcd/pull/7902/files +[pull request]:https://github.com/etcd-io/etcd/pull/7902 + +### Description + +If the follower gooroutine acquires `mu.Lock()` first and calls +`rc.release()`, it will be blocked sending over `rcNextc`. +Only the leader can `close(nextc)` to unblock the follower. +However, in order to invoke `rc.release()`, the leader needs +to acquires `mu.Lock()`. +The fix is to remove the lock and unlock around `rc.release()`. + +### Example execution + +```go +G1 G2 (leader) G3 (follower) +--------------------------------------------------------------------- +runElectionFunc() +doRounds() +wg.Wait() +. ... +. mu.Lock() +. rc.validate() +. rcNextc = nextc +. mu.Unlock() ... +. . mu.Lock() +. . rc.validate() +. . mu.Unlock() +. . mu.Lock() +. . rc.release() +. . <-rcNextc +. mu.Lock() +-------------------------G1,G2,G3 leak-------------------------- +``` + +## Grpc/1275 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#1275]|[pull request]|[patch]| Communication | Channel | + +[grpc#1275]:(grpc1275_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/1275/files +[pull request]:https://github.com/grpc/grpc-go/pull/1275 + +### Description + +Two goroutines are involved in this leak. The main goroutine +is blocked at `case <- donec`, and is waiting for the second goroutine +to close the channel. +The second goroutine is created by the main goroutine. It is blocked +when calling `stream.Read()`, which invokes `recvBufferRead.Read()`. +The second goroutine is blocked at case `i := r.recv.get()`, and it is +waiting for someone to send a message to this channel. +It is the `client.CloseSream()` method called by the main goroutine that +should send the message, but it is not. The patch is to send out this message. + +### Example execution + +```go +G1 G2 +----------------------------------------------------- +testInflightStreamClosing() +. stream.Read() +. io.ReadFull() +. <-r.recv.get() +CloseStream() +<-donec +---------------------G1, G2 leak--------------------- +``` + +## Grpc/1424 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#1424]|[pull request]|[patch]| Communication | Channel | + +[grpc#1424]:(grpc1424_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/1424/files +[pull request]:https://github.com/grpc/grpc-go/pull/1424 + +### Description + +The goroutine running `cc.lbWatcher` returns without +draining the `done` channel. + +### Example execution + +```go +G1 G2 G3 +----------------------------------------------------------------- +DialContext() . . +. cc.dopts.balancer.Notify() . +. . cc.lbWatcher() +. <-doneChan +close() +---------------------------G2 leaks------------------------------- +``` + +## Grpc/1460 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#1460]|[pull request]|[patch]| Mixed | Channel & Lock | + +[grpc#1460]:(grpc1460_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/1460/files +[pull request]:https://github.com/grpc/grpc-go/pull/1460 + +### Description + +When gRPC keepalives are enabled (which isn't the case +by default at this time) and PermitWithoutStream is false +(the default), the client can leak goroutines when transitioning +between having no active stream and having one active +stream.The keepalive() goroutine is stuck at “<-t.awakenKeepalive”, +while the main goroutine is stuck in NewStream() on t.mu.Lock(). + +### Example execution + +```go +G1 G2 +-------------------------------------------- +client.keepalive() +. client.NewStream() +t.mu.Lock() +<-t.awakenKeepalive +. t.mu.Lock() +---------------G1,G2 leak------------------- +``` + +## Grpc/3017 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#3017]|[pull request]|[patch]| Resource | Missing unlock | + +[grpc#3017]:(grpc3017_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/3017/files +[pull request]:https://github.com/grpc/grpc-go/pull/3017 + +### Description + +Line 65 is an execution path with a missing unlock. + +### Example execution + +```go +G1 G2 G3 +------------------------------------------------------------------------------------------------ +NewSubConn([1]) +ccc.mu.Lock() [L1] +sc = 1 +ccc.subConnToAddr[1] = 1 +go func() [G2] +<-done . +. ccc.RemoveSubConn(1) +. ccc.mu.Lock() +. addr = 1 +. entry = &subConnCacheEntry_grpc3017{} +. cc.subConnCache[1] = entry +. timer = time.AfterFunc() [G3] +. entry.cancel = func() +. sc = ccc.NewSubConn([1]) +. ccc.mu.Lock() [L1] +. entry.cancel() +. !timer.Stop() [true] +. entry.abortDeleting = true +. . ccc.mu.Lock() +. . <<>> +. ccc.RemoveSubConn(1) +. ccc.mu.Lock() [L1] +-------------------------------------------G1, G2 leak----------------------------------------- +``` + +## Grpc/660 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#660]|[pull request]|[patch]| Communication | Channel | + +[grpc#660]:(grpc660_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/660/files +[pull request]:https://github.com/grpc/grpc-go/pull/660 + +### Description + +The parent function could return without draining the done channel. + +### Example execution + +```go +G1 G2 helper goroutine +------------------------------------------------------------- +doCloseLoopUnary() +. bc.stop <- true +<-bc.stop +return +. done <- +----------------------G2 leak-------------------------------- +``` + +## Grpc/795 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#795]|[pull request]|[patch]| Resource | Double locking | + +[grpc#795]:(grpc795_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/795/files +[pull request]:https://github.com/grpc/grpc-go/pull/795 + +### Description + +Line 20 is an execution path with a missing unlock. + +## Grpc/862 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[grpc#862]|[pull request]|[patch]| Communication | Channel & Context | + +[grpc#862]:(grpc862_test.go) +[patch]:https://github.com/grpc/grpc-go/pull/862/files +[pull request]:https://github.com/grpc/grpc-go/pull/862 + +### Description + +When return value `conn` is `nil`, `cc(ClientConn)` is not closed. +The goroutine executing resetAddrConn is leaked. The patch is to +close `ClientConn` in `defer func()`. + +### Example execution + +```go +G1 G2 +--------------------------------------- +DialContext() +. cc.resetAddrConn() +. resetTransport() +. <-ac.ctx.Done() +--------------G2 leak------------------ +``` + +## Hugo/3251 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[hugo#3251]|[pull request]|[patch]| Resource | RWR deadlock | + +[hugo#3251]:(hugo3251_test.go) +[patch]:https://github.com/gohugoio/hugo/pull/3251/files +[pull request]:https://github.com/gohugoio/hugo/pull/3251 + +### Description + +A goroutine can hold `Lock()` at line 20 then acquire `RLock()` at +line 29. `RLock()` at line 29 will never be acquired because `Lock()` +at line 20 will never be released. + +### Example execution + +```go +G1 G2 G3 +------------------------------------------------------------------------------------------ +wg.Add(1) [W1: 1] +go func() [G2] +go func() [G3] +. resGetRemote() +. remoteURLLock.URLLock(url) +. l.Lock() [L1] +. l.m[url] = &sync.Mutex{} [L2] +. l.m[url].Lock() [L2] +. l.Unlock() [L1] +. . resGetRemote() +. . remoteURLLock.URLLock(url) +. . l.Lock() [L1] +. . l.m[url].Lock() [L2] +. remoteURLLock.URLUnlock(url) +. l.RLock() [L1] +... +wg.Wait() [W1] +----------------------------------------G1,G2,G3 leak-------------------------------------- +``` + +## Hugo/5379 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[hugo#5379]|[pull request]|[patch]| Resource | Double locking | + +[hugo#5379]:(hugo5379_test.go) +[patch]:https://github.com/gohugoio/hugo/pull/5379/files +[pull request]:https://github.com/gohugoio/hugo/pull/5379 + +### Description + +A goroutine first acquire `contentInitMu` at line 99 then +acquire the same `Mutex` at line 66 + +## Istio/16224 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[istio#16224]|[pull request]|[patch]| Mixed | Channel & Lock | + +[istio#16224]:(istio16224_test.go) +[patch]:https://github.com/istio/istio/pull/16224/files +[pull request]:https://github.com/istio/istio/pull/16224 + +### Description + +A goroutine holds a `Mutex` at line 91 and is then blocked at line 93. +Another goroutine attempts to acquire the same `Mutex` at line 101 to +further drains the same channel at 103. + +## Istio/17860 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[istio#17860]|[pull request]|[patch]| Communication | Channel | + +[istio#17860]:(istio17860_test.go) +[patch]:https://github.com/istio/istio/pull/17860/files +[pull request]:https://github.com/istio/istio/pull/17860 + +### Description + +`a.statusCh` can't be drained at line 70. + +## Istio/18454 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[istio#18454]|[pull request]|[patch]| Communication | Channel & Context | + +[istio#18454]:(istio18454_test.go) +[patch]:https://github.com/istio/istio/pull/18454/files +[pull request]:https://github.com/istio/istio/pull/18454 + +### Description + +`s.timer.Stop()` at line 56 and 61 can be called concurrency +(i.e. from their entry point at line 104 and line 66). +See [Timer](https://golang.org/pkg/time/#Timer). + +## Kubernetes/10182 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#10182]|[pull request]|[patch]| Mixed | Channel & Lock | + +[kubernetes#10182]:(kubernetes10182_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/10182/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/10182 + +### Description + +Goroutine 1 is blocked on a lock held by goroutine 3, +while goroutine 3 is blocked on sending message to `ch`, +which is read by goroutine 1. + +### Example execution + +```go +G1 G2 G3 +------------------------------------------------------------------------------- +s.Start() +s.syncBatch() +. s.SetPodStatus() +. s.podStatusesLock.Lock() +<-s.podStatusChannel <===> s.podStatusChannel <- true +. s.podStatusesLock.Unlock() +. return +s.DeletePodStatus() . +. . s.podStatusesLock.Lock() +. . s.podStatusChannel <- true +s.podStatusesLock.Lock() +-----------------------------G1,G3 leak----------------------------------------- +``` + +## Kubernetes/11298 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#11298]|[pull request]|[patch]| Communication | Channel & Condition Variable | + +[kubernetes#11298]:(kubernetes11298_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/11298/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/11298 + +### Description + +`n.node` used the `n.lock` as underlaying locker. The service loop initially +locked it, the `Notify` function tried to lock it before calling `n.node.Signal()`, +leading to a goroutine leak. `n.cond.Signal()` at line 59 and line 81 are not +guaranteed to unblock the `n.cond.Wait` at line 56. + +## Kubernetes/13135 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#13135]|[pull request]|[patch]| Resource | AB-BA deadlock | + +[kubernetes#13135]:(kubernetes13135_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/13135/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/13135 + +### Description + +```go +G1 G2 G3 +---------------------------------------------------------------------------------- +NewCacher() +watchCache.SetOnReplace() +watchCache.SetOnEvent() +. cacher.startCaching() +. c.Lock() +. c.reflector.ListAndWatch() +. r.syncWith() +. r.store.Replace() +. w.Lock() +. w.onReplace() +. cacher.initOnce.Do() +. cacher.Unlock() +return cacher . +. . c.watchCache.Add() +. . w.processEvent() +. . w.Lock() +. cacher.startCaching() . +. c.Lock() . +... +. c.Lock() +. w.Lock() +--------------------------------G2,G3 leak----------------------------------------- +``` + +## Kubernetes/1321 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#1321]|[pull request]|[patch]| Mixed | Channel & Lock | + +[kubernetes#1321]:(kubernetes1321_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/1321/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/1321 + +### Description + +This is a lock-channel bug. The first goroutine invokes +`distribute()`, which holds `m.lock.Lock()`, while blocking +at sending message to `w.result`. The second goroutine +invokes `stopWatching()` function, which can unblock the first +goroutine by closing `w.result`. However, in order to close `w.result`, +`stopWatching()` function needs to acquire `m.lock.Lock()`. + +The fix is to introduce another channel and put receive message +from the second channel in the same `select` statement as the +`w.result`. Close the second channel can unblock the first +goroutine, while no need to hold `m.lock.Lock()`. + +### Example execution + +```go +G1 G2 +---------------------------------------------- +testMuxWatcherClose() +NewMux() +. m.loop() +. m.distribute() +. m.lock.Lock() +. w.result <- true +w := m.Watch() +w.Stop() +mw.m.stopWatching() +m.lock.Lock() +---------------G1,G2 leak--------------------- +``` + +## Kubernetes/25331 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#25331]|[pull request]|[patch]| Communication | Channel & Context | + +[kubernetes#25331]:(kubernetes25331_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/25331/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/25331 + +### Description + +A potential goroutine leak occurs when an error has happened, +blocking `resultChan`, while cancelling context in `Stop()`. + +### Example execution + +```go +G1 G2 +------------------------------------ +wc.run() +. wc.Stop() +. wc.errChan <- +. wc.cancel() +<-wc.errChan +wc.cancel() +wc.resultChan <- +-------------G1 leak---------------- +``` + +## Kubernetes/26980 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#26980]|[pull request]|[patch]| Mixed | Channel & Lock | + +[kubernetes#26980]:(kubernetes26980_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/26980/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/26980 + +### Description + +A goroutine holds a `Mutex` at line 24 and blocked at line 35. +Another goroutine blocked at line 58 by acquiring the same `Mutex`. + +## Kubernetes/30872 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#30872]|[pull request]|[patch]| Resource | AB-BA deadlock | + +[kubernetes#30872]:(kubernetes30872_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/30872/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/30872 + +### Description + +The lock is acquired both at lines 92 and 157. + +## Kubernetes/38669 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#38669]|[pull request]|[patch]| Communication | Channel | + +[kubernetes#38669]:(kubernetes38669_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/38669/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/38669 + +### Description + +No sender for line 33. + +## Kubernetes/5316 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#5316]|[pull request]|[patch]| Communication | Channel | + +[kubernetes#5316]:(kubernetes5316_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/5316/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/5316 + +### Description + +If the main goroutine selects a case that doesn’t consumes +the channels, the anonymous goroutine will be blocked on sending +to channel. + +### Example execution + +```go +G1 G2 +-------------------------------------- +finishRequest() +. fn() +time.After() +. errCh<-/ch<- +--------------G2 leaks---------------- +``` + +## Kubernetes/58107 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#58107]|[pull request]|[patch]| Resource | RWR deadlock | + +[kubernetes#58107]:(kubernetes58107_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/58107/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/58107 + +### Description + +The rules for read and write lock: allows concurrent read lock; +write lock has higher priority than read lock. + +There are two queues (queue 1 and queue 2) involved in this bug, +and the two queues are protected by the same read-write lock +(`rq.workerLock.RLock()`). Before getting an element from queue 1 or +queue 2, `rq.workerLock.RLock()` is acquired. If the queue is empty, +`cond.Wait()` will be invoked. There is another goroutine (goroutine D), +which will periodically invoke `rq.workerLock.Lock()`. Under the following +situation, deadlock will happen. Queue 1 is empty, so that some goroutines +hold `rq.workerLock.RLock()`, and block at `cond.Wait()`. Goroutine D is +blocked when acquiring `rq.workerLock.Lock()`. Some goroutines try to process +jobs in queue 2, but they are blocked when acquiring `rq.workerLock.RLock()`, +since write lock has a higher priority. + +The fix is to not acquire `rq.workerLock.RLock()`, while pulling data +from any queue. Therefore, when a goroutine is blocked at `cond.Wait()`, +`rq.workLock.RLock()` is not held. + +### Example execution + +```go +G3 G4 G5 +-------------------------------------------------------------------- +. . Sync() +rq.workerLock.RLock() . . +q.cond.Wait() . . +. . rq.workerLock.Lock() +. rq.workerLock.RLock() +. q.cond.L.Lock() +-----------------------------G3,G4,G5 leak----------------------------- +``` + +## Kubernetes/62464 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#62464]|[pull request]|[patch]| Resource | RWR deadlock | + +[kubernetes#62464]:(kubernetes62464_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/62464/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/62464 + +### Description + +This is another example for recursive read lock bug. It has +been noticed by the go developers that RLock should not be +recursively used in the same thread. + +### Example execution + +```go +G1 G2 +-------------------------------------------------------- +m.reconcileState() +m.state.GetCPUSetOrDefault() +s.RLock() +s.GetCPUSet() +. p.RemoveContainer() +. s.GetDefaultCPUSet() +. s.SetDefaultCPUSet() +. s.Lock() +s.RLock() +---------------------G1,G2 leak-------------------------- +``` + +## Kubernetes/6632 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#6632]|[pull request]|[patch]| Mixed | Channel & Lock | + +[kubernetes#6632]:(kubernetes6632_test.go) +[patch]:https://github.com/kubernetes/kubernetes/pull/6632/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/6632 + +### Description + +When `resetChan` is full, `WriteFrame` holds the lock and blocks +on the channel. Then `monitor()` fails to close the `resetChan` +because the lock is already held by `WriteFrame`. + + +### Example execution + +```go +G1 G2 helper goroutine +---------------------------------------------------------------- +i.monitor() +<-i.conn.closeChan +. i.WriteFrame() +. i.writeLock.Lock() +. i.resetChan <- +. . i.conn.closeChan<- +i.writeLock.Lock() +----------------------G1,G2 leak-------------------------------- +``` + +## Kubernetes/70277 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[kubernetes#70277]|[pull request]|[patch]| Communication | Channel | + +[kubernetes#70277]:kubernetes70277_test.go +[patch]:https://github.com/kubernetes/kubernetes/pull/70277/files +[pull request]:https://github.com/kubernetes/kubernetes/pull/70277 + +### Description + +`wait.poller()` returns a function with type `WaitFunc`. +the function creates a goroutine and the goroutine only +quits when after or done closed. + +The `doneCh` defined at line 70 is never closed. + +## Moby/17176 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#17176]|[pull request]|[patch]| Resource | Double locking | + +[moby#17176]:(moby17176_test.go) +[patch]:https://github.com/moby/moby/pull/17176/files +[pull request]:https://github.com/moby/moby/pull/17176 + +### Description + +`devices.nrDeletedDevices` takes `devices.Lock()` but does +not release it (line 36) if there are no deleted devices. This will block +other goroutines trying to acquire `devices.Lock()`. + +## Moby/21233 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#21233]|[pull request]|[patch]| Communication | Channel | + +[moby#21233]:(moby21233_test.go) +[patch]:https://github.com/moby/moby/pull/21233/files +[pull request]:https://github.com/moby/moby/pull/21233 + +### Description + +This test was checking that it received every progress update that was +produced. But delivery of these intermediate progress updates is not +guaranteed. A new update can overwrite the previous one if the previous +one hasn't been sent to the channel yet. + +The call to `t.Fatalf` terminated the current goroutine which was consuming +the channel, which caused a deadlock and eventual test timeout rather +than a proper failure message. + +### Example execution + +```go +G1 G2 G3 +---------------------------------------------------------- +testTransfer() . . +tm.Transfer() . . +t.Watch() . . +. WriteProgress() . +. ProgressChan<- . +. . <-progressChan +. ... ... +. return . +. <-progressChan +<-watcher.running +----------------------G1,G3 leak-------------------------- +``` + +## Moby/25384 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#25384]|[pull request]|[patch]| Mixed | Misuse WaitGroup | + +[moby#25384]:(moby25384_test.go) +[patch]:https://github.com/moby/moby/pull/25384/files +[pull request]:https://github.com/moby/moby/pull/25384 + +### Description + +When `n=1` (where `n` is `len(pm.plugins)`), the location of `group.Wait()` doesn’t matter. +When `n > 1`, `group.Wait()` is invoked in each iteration. Whenever +`group.Wait()` is invoked, it waits for `group.Done()` to be executed `n` times. +However, `group.Done()` is only executed once in one iteration. + +Misuse of sync.WaitGroup + +## Moby/27782 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#27782]|[pull request]|[patch]| Communication | Channel & Condition Variable | + +[moby#27782]:(moby27782_test.go) +[patch]:https://github.com/moby/moby/pull/27782/files +[pull request]:https://github.com/moby/moby/pull/27782 + +### Description + +### Example execution + +```go +G1 G2 G3 +----------------------------------------------------------------------- +InitializeStdio() +startLogging() +l.ReadLogs() +NewLogWatcher() +. l.readLogs() +container.Reset() . +LogDriver.Close() . +r.Close() . +close(w.closeNotifier) . +. followLogs(logWatcher) +. watchFile() +. New() +. NewEventWatcher() +. NewWatcher() +. . w.readEvents() +. . event.ignoreLinux() +. . return false +. <-logWatcher.WatchClose() . +. fileWatcher.Remove() . +. w.cv.Wait() . +. . w.Events <- event +------------------------------G2,G3 leak------------------------------- +``` + +## Moby/28462 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#28462]|[pull request]|[patch]| Mixed | Channel & Lock | + +[moby#28462]:(moby28462_test.go) +[patch]:https://github.com/moby/moby/pull/28462/files +[pull request]:https://github.com/moby/moby/pull/28462 + +### Description + +One goroutine may acquire a lock and try to send a message over channel `stop`, +while the other will try to acquire the same lock. With the wrong ordering, +both goroutines will leak. + +### Example execution + +```go +G1 G2 +-------------------------------------------------------------- +monitor() +handleProbeResult() +. d.StateChanged() +. c.Lock() +. d.updateHealthMonitorElseBranch() +. h.CloseMonitorChannel() +. s.stop <- struct{}{} +c.Lock() +----------------------G1,G2 leak------------------------------ +``` + +## Moby/30408 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#30408]|[pull request]|[patch]| Communication | Condition Variable | + +[moby#30408]:(moby30408_test.go) +[patch]:https://github.com/moby/moby/pull/30408/files +[pull request]:https://github.com/moby/moby/pull/30408 + +### Description + +`Wait()` at line 22 has no corresponding `Signal()` or `Broadcast()`. + +### Example execution + +```go +G1 G2 +------------------------------------------ +testActive() +. p.waitActive() +. p.activateWait.L.Lock() +. p.activateWait.Wait() +<-done +-----------------G1,G2 leak--------------- +``` + +## Moby/33781 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#33781]|[pull request]|[patch]| Communication | Channel & Context | + +[moby#33781]:(moby33781_test.go) +[patch]:https://github.com/moby/moby/pull/33781/files +[pull request]:https://github.com/moby/moby/pull/33781 + +### Description + +The goroutine created using an anonymous function is blocked +sending a message over an unbuffered channel. However there +exists a path in the parent goroutine where the parent function +will return without draining the channel. + +### Example execution + +```go +G1 G2 G3 +---------------------------------------- +monitor() . +<-time.After() . +. . +<-stop stop<- +. +cancelProbe() +return +. result<- +----------------G3 leak------------------ +``` + +## Moby/36114 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#36114]|[pull request]|[patch]| Resource | Double locking | + +[moby#36114]:(moby36114_test.go) +[patch]:https://github.com/moby/moby/pull/36114/files +[pull request]:https://github.com/moby/moby/pull/36114 + +### Description + +The the lock for the struct svm has already been locked when calling +`svm.hotRemoveVHDsAtStart()`. + +## Moby/4951 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#4951]|[pull request]|[patch]| Resource | AB-BA deadlock | + +[moby#4951]:(moby4951_test.go) +[patch]:https://github.com/moby/moby/pull/4951/files +[pull request]:https://github.com/moby/moby/pull/4951 + +### Description + +The root cause and patch is clearly explained in the commit +description. The global lock is `devices.Lock()`, and the device +lock is `baseInfo.lock.Lock()`. It is very likely that this bug +can be reproduced. + +## Moby/7559 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[moby#7559]|[pull request]|[patch]| Resource | Double locking | + +[moby#7559]:(moby7559_test.go) +[patch]:https://github.com/moby/moby/pull/7559/files +[pull request]:https://github.com/moby/moby/pull/7559 + +### Description + +Line 25 is missing a call to `.Unlock`. + +### Example execution + +```go +G1 +--------------------------- +proxy.connTrackLock.Lock() +if err != nil { continue } +proxy.connTrackLock.Lock() +-----------G1 leaks-------- +``` + +## Serving/2137 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[serving#2137]|[pull request]|[patch]| Mixed | Channel & Lock | + +[serving#2137]:(serving2137_test.go) +[patch]:https://github.com/ knative/serving/pull/2137/files +[pull request]:https://github.com/ knative/serving/pull/2137 + +### Description + +### Example execution + +```go +G1 G2 G3 +---------------------------------------------------------------------------------- +b.concurrentRequests(2) . . +b.concurrentRequest() . . +r.lock.Lock() . . +. start.Done() . +start.Wait() . . +b.concurrentRequest() . . +r.lock.Lock() . . +. . start.Done() +start.Wait() . . +unlockAll(locks) . . +unlock(lc) . . +req.lock.Unlock() . . +ok := <-req.accepted . . +. b.Maybe() . +. b.activeRequests <- t . +. thunk() . +. r.lock.Lock() . +. . b.Maybe() +. . b.activeRequests <- t +----------------------------G1,G2,G3 leak----------------------------------------- +``` + +## Syncthing/4829 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[syncthing#4829]|[pull request]|[patch]| Resource | Double locking | + +[syncthing#4829]:(syncthing4829_test.go) +[patch]:https://github.com/syncthing/syncthing/pull/4829/files +[pull request]:https://github.com/syncthing/syncthing/pull/4829 + +### Description + +Double locking at line 17 and line 30. + +### Example execution + +```go +G1 +--------------------------- +mapping.clearAddresses() +m.mut.Lock() [L2] +m.notify(...) +m.mut.RLock() [L2] +----------G1 leaks--------- +``` + +## Syncthing/5795 + +| Bug ID | Ref | Patch | Type | Sub-type | +| ---- | ---- | ---- | ---- | ---- | +|[syncthing#5795]|[pull request]|[patch]| Communication | Channel | + +[syncthing#5795]:(syncthing5795_test.go) +[patch]:https://github.com/syncthing/syncthing/pull/5795/files +[pull request]:https://github.com/syncthing/syncthing/pull/5795 + +### Description + +`<-c.dispatcherLoopStopped` at line 82 is blocking forever because +`dispatcherLoop()` is blocking at line 72. + +### Example execution + +```go +G1 G2 +-------------------------------------------------------------- +c.Start() +go c.dispatcherLoop() [G3] +. select [<-c.inbox, <-c.closed] +c.inbox <- <================> [<-c.inbox] +<-c.dispatcherLoopStopped . +. default +. c.ccFn()/c.Close() +. close(c.closed) +. <-c.dispatcherLoopStopped +---------------------G1,G2 leak------------------------------- +``` \ No newline at end of file diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10214.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10214.go new file mode 100644 index 00000000000000..4f5ef3b0fc32fc --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10214.go @@ -0,0 +1,145 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/10214 + * Buggy version: 7207111aa3a43df0552509365fdec741a53f873f + * fix commit-id: 27e863d90ab0660494778f1c35966cc5ddc38e32 + * Flaky: 3/100 + * Description: This goroutine leak is caused by different order when acquiring + * coalescedMu.Lock() and raftMu.Lock(). The fix is to refactor sendQueuedHeartbeats() + * so that cockroachdb can unlock coalescedMu before locking raftMu. + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" + "unsafe" +) + +func init() { + register("Cockroach10214", Cockroach10214) +} + +type Store_cockroach10214 struct { + coalescedMu struct { + sync.Mutex // L1 + heartbeatResponses []int + } + mu struct { + replicas map[int]*Replica_cockroach10214 + } +} + +func (s *Store_cockroach10214) sendQueuedHeartbeats() { + s.coalescedMu.Lock() // L1 acquire + defer s.coalescedMu.Unlock() // L2 release + for i := 0; i < len(s.coalescedMu.heartbeatResponses); i++ { + s.sendQueuedHeartbeatsToNode() // L2 + } +} + +func (s *Store_cockroach10214) sendQueuedHeartbeatsToNode() { + for i := 0; i < len(s.mu.replicas); i++ { + r := s.mu.replicas[i] + r.reportUnreachable() // L2 + } +} + +type Replica_cockroach10214 struct { + raftMu sync.Mutex // L2 + mu sync.Mutex // L3 + store *Store_cockroach10214 +} + +func (r *Replica_cockroach10214) reportUnreachable() { + r.raftMu.Lock() // L2 acquire + time.Sleep(time.Millisecond) + defer r.raftMu.Unlock() // L2 release +} + +func (r *Replica_cockroach10214) tick() { + r.raftMu.Lock() // L2 acquire + defer r.raftMu.Unlock() // L2 release + r.tickRaftMuLocked() +} + +func (r *Replica_cockroach10214) tickRaftMuLocked() { + r.mu.Lock() // L3 acquire + defer r.mu.Unlock() // L3 release + if r.maybeQuiesceLocked() { + return + } +} + +func (r *Replica_cockroach10214) maybeQuiesceLocked() bool { + for i := 0; i < 2; i++ { + if !r.maybeCoalesceHeartbeat() { + return true + } + } + return false +} + +func (r *Replica_cockroach10214) maybeCoalesceHeartbeat() bool { + msgtype := uintptr(unsafe.Pointer(r)) % 3 + switch msgtype { + case 0, 1, 2: + r.store.coalescedMu.Lock() // L1 acquire + default: + return false + } + r.store.coalescedMu.Unlock() // L1 release + return true +} + +func Cockroach10214() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 1000; i++ { + go func() { + store := &Store_cockroach10214{} + responses := &store.coalescedMu.heartbeatResponses + *responses = append(*responses, 1, 2) + store.mu.replicas = make(map[int]*Replica_cockroach10214) + + rp1 := &Replica_cockroach10214{ // L2,3[0] + store: store, + } + rp2 := &Replica_cockroach10214{ // L2,3[1] + store: store, + } + store.mu.replicas[0] = rp1 + store.mu.replicas[1] = rp2 + + go store.sendQueuedHeartbeats() // G1 + go rp1.tick() // G2 + }() + } +} + +// Example of goroutine leak trace: +// +// G1 G2 +//------------------------------------------------------------------------------------ +// s.sendQueuedHeartbeats() . +// s.coalescedMu.Lock() [L1] . +// s.sendQueuedHeartbeatsToNode() . +// s.mu.replicas[0].reportUnreachable() . +// s.mu.replicas[0].raftMu.Lock() [L2] . +// . s.mu.replicas[0].tick() +// . s.mu.replicas[0].raftMu.Lock() [L2] +// . s.mu.replicas[0].tickRaftMuLocked() +// . s.mu.replicas[0].mu.Lock() [L3] +// . s.mu.replicas[0].maybeQuiesceLocked() +// . s.mu.replicas[0].maybeCoalesceHeartbeat() +// . s.coalescedMu.Lock() [L1] +//--------------------------------G1,G2 leak------------------------------------------ \ No newline at end of file diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1055.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1055.go new file mode 100644 index 00000000000000..687baed25a2a44 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1055.go @@ -0,0 +1,115 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime/pprof" + "sync" + "sync/atomic" + "time" +) + +func init() { + register("Cockroach1055", Cockroach1055) +} + +type Stopper_cockroach1055 struct { + stopper chan struct{} + stop sync.WaitGroup + mu sync.Mutex + draining int32 + drain sync.WaitGroup +} + +func (s *Stopper_cockroach1055) AddWorker() { + s.stop.Add(1) +} + +func (s *Stopper_cockroach1055) ShouldStop() <-chan struct{} { + if s == nil { + return nil + } + return s.stopper +} + +func (s *Stopper_cockroach1055) SetStopped() { + if s != nil { + s.stop.Done() + } +} + +func (s *Stopper_cockroach1055) Quiesce() { + s.mu.Lock() + defer s.mu.Unlock() + s.draining = 1 + s.drain.Wait() + s.draining = 0 +} + +func (s *Stopper_cockroach1055) Stop() { + s.mu.Lock() // L1 + defer s.mu.Unlock() + atomic.StoreInt32(&s.draining, 1) + s.drain.Wait() + close(s.stopper) + s.stop.Wait() +} + +func (s *Stopper_cockroach1055) StartTask() bool { + if atomic.LoadInt32(&s.draining) == 0 { + s.mu.Lock() + defer s.mu.Unlock() + s.drain.Add(1) + return true + } + return false +} + +func NewStopper_cockroach1055() *Stopper_cockroach1055 { + return &Stopper_cockroach1055{ + stopper: make(chan struct{}), + } +} + +func Cockroach1055() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i <= 1000; i++ { + go func() { // G1 + var stoppers []*Stopper_cockroach1055 + for i := 0; i < 2; i++ { + stoppers = append(stoppers, NewStopper_cockroach1055()) + } + + for i := range stoppers { + s := stoppers[i] + s.AddWorker() + go func() { // G2 + s.StartTask() + <-s.ShouldStop() + s.SetStopped() + }() + } + + done := make(chan struct{}) + go func() { // G3 + for _, s := range stoppers { + s.Quiesce() + } + for _, s := range stoppers { + s.Stop() + } + close(done) + }() + + <-done + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10790.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10790.go new file mode 100644 index 00000000000000..636f45b3e0e765 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach10790.go @@ -0,0 +1,98 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/10790 + * Buggy version: 96b5452557ebe26bd9d85fe7905155009204d893 + * fix commit-id: f1a5c19125c65129b966fbdc0e6408e8df214aba + * Flaky: 28/100 + * Description: + * It is possible that a message from ctxDone will make the function beginCmds + * returns without draining the channel ch, so that goroutines created by anonymous + * function will leak. + */ + +package main + +import ( + "context" + "os" + "runtime/pprof" + "time" +) + +func init() { + register("Cockroach10790", Cockroach10790) +} + +type Replica_cockroach10790 struct { + chans []chan bool +} + +func (r *Replica_cockroach10790) beginCmds(ctx context.Context) { + ctxDone := ctx.Done() + for _, ch := range r.chans { + select { + case <-ch: + case <-ctxDone: + go func() { // G3 + for _, ch := range r.chans { + <-ch + } + }() + } + } +} + +func (r *Replica_cockroach10790) sendChans(ctx context.Context) { + for _, ch := range r.chans { + select { + case ch <- true: + case <-ctx.Done(): + return + } + } +} + +func NewReplica_cockroach10790() *Replica_cockroach10790 { + r := &Replica_cockroach10790{} + r.chans = append(r.chans, make(chan bool), make(chan bool)) + return r +} + +// Example of goroutine leak trace: +// +// G1 G2 G3 helper goroutine +//-------------------------------------------------------------------------------------- +// . . r.sendChans() +// r.beginCmds() . . +// . . ch1 <- +// <-ch1 <================================================> ch1 <- +// . . select [ch2<-, <-ctx.Done()] +// . cancel() . +// . <> [<-ctx.Done()] ==> return +// . <> +// go func() [G3] . +// . <-ch1 +// ------------------------------G3 leaks---------------------------------------------- +// + +func Cockroach10790() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + r := NewReplica_cockroach10790() + ctx, cancel := context.WithCancel(context.Background()) + go r.sendChans(ctx) // helper goroutine + go r.beginCmds(ctx) // G1 + go cancel() // G2 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13197.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13197.go new file mode 100644 index 00000000000000..a0a9a792676692 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13197.go @@ -0,0 +1,82 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/13197 + * Buggy version: fff27aedabafe20cef57f75905fe340cab48c2a4 + * fix commit-id: 9bf770cd8f6eaff5441b80d3aec1a5614e8747e1 + * Flaky: 100/100 + * Description: One goroutine executing (*Tx).awaitDone() blocks + * waiting for a signal over context.Done() that never comes. + */ +package main + +import ( + "context" + "os" + "runtime" + "runtime/pprof" +) + +func init() { + register("Cockroach13197", Cockroach13197) +} + +type DB_cockroach13197 struct{} + +func (db *DB_cockroach13197) begin(ctx context.Context) *Tx_cockroach13197 { + ctx, cancel := context.WithCancel(ctx) + tx := &Tx_cockroach13197{ + cancel: cancel, + ctx: ctx, + } + go tx.awaitDone() // G2 + return tx +} + +type Tx_cockroach13197 struct { + cancel context.CancelFunc + ctx context.Context +} + +func (tx *Tx_cockroach13197) awaitDone() { + <-tx.ctx.Done() +} + +func (tx *Tx_cockroach13197) Rollback() { + tx.rollback() +} + +func (tx *Tx_cockroach13197) rollback() { + tx.close() +} + +func (tx *Tx_cockroach13197) close() { + tx.cancel() +} + +// Example of goroutine leak trace: +// +// G1 G2 +//-------------------------------- +// begin() +// . awaitDone() +// <> . +// <-tx.ctx.Done() +//------------G2 leak------------- + +func Cockroach13197() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + db := &DB_cockroach13197{} + db.begin(context.Background()) // G1 +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13755.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13755.go new file mode 100644 index 00000000000000..5ef6fa1e28a06d --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach13755.go @@ -0,0 +1,66 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/13755 + * Buggy version: 7acb881bbb8f23e87b69fce9568d9a3316b5259c + * fix commit-id: ef906076adc1d0e3721944829cfedfed51810088 + * Flaky: 100/100 + */ + +package main + +import ( + "context" + "os" + "runtime" + "runtime/pprof" +) + +func init() { + register("Cockroach13755", Cockroach13755) +} + +type Rows_cockroach13755 struct { + cancel context.CancelFunc +} + +func (rs *Rows_cockroach13755) initContextClose(ctx context.Context) { + ctx, rs.cancel = context.WithCancel(ctx) + go rs.awaitDone(ctx) +} + +func (rs *Rows_cockroach13755) awaitDone(ctx context.Context) { + <-ctx.Done() + rs.close(ctx.Err()) +} + +func (rs *Rows_cockroach13755) close(err error) { + rs.cancel() +} + +// Example of goroutine leak trace: +// +// G1 G2 +//---------------------------------------- +// initContextClose() +// . awaitDone() +// <> . +// <-tx.ctx.Done() +//----------------G2 leak----------------- + +func Cockroach13755() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + rs := &Rows_cockroach13755{} + rs.initContextClose(context.Background()) +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1462.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1462.go new file mode 100644 index 00000000000000..108d7884a3d82b --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach1462.go @@ -0,0 +1,167 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Cockroach1462", Cockroach1462) +} + +type Stopper_cockroach1462 struct { + stopper chan struct{} + stopped chan struct{} + stop sync.WaitGroup + mu sync.Mutex + drain *sync.Cond + draining bool + numTasks int +} + +func NewStopper_cockroach1462() *Stopper_cockroach1462 { + s := &Stopper_cockroach1462{ + stopper: make(chan struct{}), + stopped: make(chan struct{}), + } + s.drain = sync.NewCond(&s.mu) + return s +} + +func (s *Stopper_cockroach1462) RunWorker(f func()) { + s.AddWorker() + go func() { // G2, G3 + defer s.SetStopped() + f() + }() +} + +func (s *Stopper_cockroach1462) AddWorker() { + s.stop.Add(1) +} +func (s *Stopper_cockroach1462) StartTask() bool { + s.mu.Lock() + runtime.Gosched() + defer s.mu.Unlock() + if s.draining { + return false + } + s.numTasks++ + return true +} + +func (s *Stopper_cockroach1462) FinishTask() { + s.mu.Lock() + runtime.Gosched() + defer s.mu.Unlock() + s.numTasks-- + s.drain.Broadcast() +} +func (s *Stopper_cockroach1462) SetStopped() { + if s != nil { + s.stop.Done() + } +} +func (s *Stopper_cockroach1462) ShouldStop() <-chan struct{} { + if s == nil { + return nil + } + return s.stopper +} + +func (s *Stopper_cockroach1462) Quiesce() { + s.mu.Lock() + runtime.Gosched() + defer s.mu.Unlock() + s.draining = true + for s.numTasks > 0 { + // Unlock s.mu, wait for the signal, and lock s.mu. + s.drain.Wait() + } +} + +func (s *Stopper_cockroach1462) Stop() { + s.Quiesce() + close(s.stopper) + s.stop.Wait() + s.mu.Lock() + runtime.Gosched() + defer s.mu.Unlock() + close(s.stopped) +} + +type interceptMessage_cockroach1462 int + +type localInterceptableTransport_cockroach1462 struct { + mu sync.Mutex + Events chan interceptMessage_cockroach1462 + stopper *Stopper_cockroach1462 +} + +func (lt *localInterceptableTransport_cockroach1462) Close() {} + +type Transport_cockroach1462 interface { + Close() +} + +func NewLocalInterceptableTransport_cockroach1462(stopper *Stopper_cockroach1462) Transport_cockroach1462 { + lt := &localInterceptableTransport_cockroach1462{ + Events: make(chan interceptMessage_cockroach1462), + stopper: stopper, + } + lt.start() + return lt +} + +func (lt *localInterceptableTransport_cockroach1462) start() { + lt.stopper.RunWorker(func() { + for { + select { + case <-lt.stopper.ShouldStop(): + return + default: + lt.Events <- interceptMessage_cockroach1462(0) + } + } + }) +} + +func processEventsUntil_cockroach1462(ch <-chan interceptMessage_cockroach1462, stopper *Stopper_cockroach1462) { + for { + select { + case _, ok := <-ch: + runtime.Gosched() + if !ok { + return + } + case <-stopper.ShouldStop(): + return + } + } +} + +func Cockroach1462() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(2000 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i <= 1000; i++ { + go func() { // G1 + stopper := NewStopper_cockroach1462() + transport := NewLocalInterceptableTransport_cockroach1462(stopper).(*localInterceptableTransport_cockroach1462) + stopper.RunWorker(func() { + processEventsUntil_cockroach1462(transport.Events, stopper) + }) + stopper.Stop() + }() + } +} + diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach16167.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach16167.go new file mode 100644 index 00000000000000..4cd14c7a5b3ac5 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach16167.go @@ -0,0 +1,108 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/16167 + * Buggy version: 36fa784aa846b46c29e077634c4e362635f6e74a + * fix commit-id: d064942b067ab84628f79cbfda001fa3138d8d6e + * Flaky: 1/100 + */ + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Cockroach16167", Cockroach16167) +} + +type PreparedStatements_cockroach16167 struct { + session *Session_cockroach16167 +} + +func (ps PreparedStatements_cockroach16167) New(e *Executor_cockroach16167) { + e.Prepare(ps.session) +} + +type Session_cockroach16167 struct { + PreparedStatements PreparedStatements_cockroach16167 +} + +func (s *Session_cockroach16167) resetForBatch(e *Executor_cockroach16167) { + e.getDatabaseCache() +} + +type Executor_cockroach16167 struct { + systemConfigCond *sync.Cond + systemConfigMu sync.RWMutex // L1 +} + +func (e *Executor_cockroach16167) Start() { + e.updateSystemConfig() +} + +func (e *Executor_cockroach16167) execParsed(session *Session_cockroach16167) { + e.systemConfigCond.L.Lock() // Same as e.systemConfigMu.RLock() + runtime.Gosched() + defer e.systemConfigCond.L.Unlock() + runTxnAttempt_cockroach16167(e, session) +} + +func (e *Executor_cockroach16167) execStmtsInCurrentTxn(session *Session_cockroach16167) { + e.execStmtInOpenTxn(session) +} + +func (e *Executor_cockroach16167) execStmtInOpenTxn(session *Session_cockroach16167) { + session.PreparedStatements.New(e) +} + +func (e *Executor_cockroach16167) Prepare(session *Session_cockroach16167) { + session.resetForBatch(e) +} + +func (e *Executor_cockroach16167) getDatabaseCache() { + e.systemConfigMu.RLock() + defer e.systemConfigMu.RUnlock() +} + +func (e *Executor_cockroach16167) updateSystemConfig() { + e.systemConfigMu.Lock() + runtime.Gosched() + defer e.systemConfigMu.Unlock() +} + +func runTxnAttempt_cockroach16167(e *Executor_cockroach16167, session *Session_cockroach16167) { + e.execStmtsInCurrentTxn(session) +} + +func NewExectorAndSession_cockroach16167() (*Executor_cockroach16167, *Session_cockroach16167) { + session := &Session_cockroach16167{} + session.PreparedStatements = PreparedStatements_cockroach16167{session} + e := &Executor_cockroach16167{} + return e, session +} + +func Cockroach16167() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { // G1 + e, s := NewExectorAndSession_cockroach16167() + e.systemConfigCond = sync.NewCond(e.systemConfigMu.RLocker()) + go e.Start() // G2 + e.execParsed(s) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach18101.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach18101.go new file mode 100644 index 00000000000000..17b03203304ac7 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach18101.go @@ -0,0 +1,60 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/18101 + * Buggy version: f7a8e2f57b6bcf00b9abaf3da00598e4acd3a57f + * fix commit-id: 822bd176cc725c6b50905ea615023200b395e14f + * Flaky: 100/100 + */ + +package main + +import ( + "context" + "os" + "runtime/pprof" + "time" +) + +func init() { + register("Cockroach18101", Cockroach18101) +} + +const chanSize_cockroach18101 = 6 + +func restore_cockroach18101(ctx context.Context) bool { + readyForImportCh := make(chan bool, chanSize_cockroach18101) + go func() { // G2 + defer close(readyForImportCh) + splitAndScatter_cockroach18101(ctx, readyForImportCh) + }() + for readyForImportSpan := range readyForImportCh { + select { + case <-ctx.Done(): + return readyForImportSpan + } + } + return true +} + +func splitAndScatter_cockroach18101(ctx context.Context, readyForImportCh chan bool) { + for i := 0; i < chanSize_cockroach18101+2; i++ { + readyForImportCh <- (false || i != 0) + } +} + +func Cockroach18101() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + ctx, cancel := context.WithCancel(context.Background()) + go restore_cockroach18101(ctx) // G1 + go cancel() // helper goroutine + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach2448.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach2448.go new file mode 100644 index 00000000000000..a7544bc8a46a18 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach2448.go @@ -0,0 +1,125 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "time" +) + +func init() { + register("Cockroach2448", Cockroach2448) +} + +type Stopper_cockroach2448 struct { + Done chan bool +} + +func (s *Stopper_cockroach2448) ShouldStop() <-chan bool { + return s.Done +} + +type EventMembershipChangeCommitted_cockroach2448 struct { + Callback func() +} + +type MultiRaft_cockroach2448 struct { + stopper *Stopper_cockroach2448 + Events chan interface{} + callbackChan chan func() +} + +// sendEvent can be invoked many times +func (m *MultiRaft_cockroach2448) sendEvent(event interface{}) { + select { + case m.Events <- event: // Waiting for events consumption + case <-m.stopper.ShouldStop(): + } +} + +type state_cockroach2448 struct { + *MultiRaft_cockroach2448 +} + +func (s *state_cockroach2448) start() { + for { + select { + case <-s.stopper.ShouldStop(): + return + case cb := <-s.callbackChan: + cb() + default: + s.handleWriteResponse() + time.Sleep(100 * time.Microsecond) + } + } +} + +func (s *state_cockroach2448) handleWriteResponse() { + s.sendEvent(&EventMembershipChangeCommitted_cockroach2448{ + Callback: func() { + select { + case s.callbackChan <- func() { // Waiting for callbackChan consumption + time.Sleep(time.Nanosecond) + }: + case <-s.stopper.ShouldStop(): + } + }, + }) +} + +type Store_cockroach2448 struct { + multiraft *MultiRaft_cockroach2448 +} + +func (s *Store_cockroach2448) processRaft() { + for { + select { + case e := <-s.multiraft.Events: + switch e := e.(type) { + case *EventMembershipChangeCommitted_cockroach2448: + callback := e.Callback + runtime.Gosched() + if callback != nil { + callback() // Waiting for callbackChan consumption + } + } + case <-s.multiraft.stopper.ShouldStop(): + return + } + } +} + +func NewStoreAndState_cockroach2448() (*Store_cockroach2448, *state_cockroach2448) { + stopper := &Stopper_cockroach2448{ + Done: make(chan bool), + } + mltrft := &MultiRaft_cockroach2448{ + stopper: stopper, + Events: make(chan interface{}), + callbackChan: make(chan func()), + } + st := &state_cockroach2448{mltrft} + s := &Store_cockroach2448{mltrft} + return s, st +} + +func Cockroach2448() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 1000; i++ { + go func() { + s, st := NewStoreAndState_cockroach2448() + go s.processRaft() // G1 + go st.start() // G2 + }() + } +} + diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach24808.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach24808.go new file mode 100644 index 00000000000000..a916d3c928ee2e --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach24808.go @@ -0,0 +1,78 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Cockroach24808", Cockroach24808) +} + +type Compactor_cockroach24808 struct { + ch chan struct{} +} + +type Stopper_cockroach24808 struct { + stop sync.WaitGroup + stopper chan struct{} +} + +func (s *Stopper_cockroach24808) RunWorker(ctx context.Context, f func(context.Context)) { + s.stop.Add(1) + go func() { + defer s.stop.Done() + f(ctx) + }() +} + +func (s *Stopper_cockroach24808) ShouldStop() <-chan struct{} { + if s == nil { + return nil + } + return s.stopper +} + +func (s *Stopper_cockroach24808) Stop() { + close(s.stopper) +} + +func (c *Compactor_cockroach24808) Start(ctx context.Context, stopper *Stopper_cockroach24808) { + c.ch <- struct{}{} + stopper.RunWorker(ctx, func(ctx context.Context) { + for { + select { + case <-stopper.ShouldStop(): + return + case <-c.ch: + } + } + }) +} + +func Cockroach24808() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { // G1 + stopper := &Stopper_cockroach24808{stopper: make(chan struct{})} + defer stopper.Stop() + + compactor := &Compactor_cockroach24808{ch: make(chan struct{}, 1)} + compactor.ch <- struct{}{} + + compactor.Start(context.Background(), stopper) + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach25456.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach25456.go new file mode 100644 index 00000000000000..b9259c9f918293 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach25456.go @@ -0,0 +1,92 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" +) + +func init() { + register("Cockroach25456", Cockroach25456) +} + +type Stopper_cockroach25456 struct { + quiescer chan struct{} +} + +func (s *Stopper_cockroach25456) ShouldQuiesce() <-chan struct{} { + if s == nil { + return nil + } + return s.quiescer +} + +func NewStopper_cockroach25456() *Stopper_cockroach25456 { + return &Stopper_cockroach25456{quiescer: make(chan struct{})} +} + +type Store_cockroach25456 struct { + stopper *Stopper_cockroach25456 + consistencyQueue *consistencyQueue_cockroach25456 +} + +func (s *Store_cockroach25456) Stopper() *Stopper_cockroach25456 { + return s.stopper +} + +type Replica_cockroach25456 struct { + store *Store_cockroach25456 +} + +func NewReplica_cockroach25456(store *Store_cockroach25456) *Replica_cockroach25456 { + return &Replica_cockroach25456{store: store} +} + +type consistencyQueue_cockroach25456 struct{} + +func (q *consistencyQueue_cockroach25456) process(repl *Replica_cockroach25456) { + <-repl.store.Stopper().ShouldQuiesce() +} + +func newConsistencyQueue_cockroach25456() *consistencyQueue_cockroach25456 { + return &consistencyQueue_cockroach25456{} +} + +type testContext_cockroach25456 struct { + store *Store_cockroach25456 + repl *Replica_cockroach25456 +} + +func (tc *testContext_cockroach25456) StartWithStoreConfig(stopper *Stopper_cockroach25456) { + if tc.store == nil { + tc.store = &Store_cockroach25456{ + consistencyQueue: newConsistencyQueue_cockroach25456(), + } + } + tc.store.stopper = stopper + tc.repl = NewReplica_cockroach25456(tc.store) +} + +func Cockroach25456() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { // G1 + stopper := NewStopper_cockroach25456() + tc := testContext_cockroach25456{} + tc.StartWithStoreConfig(stopper) + + for i := 0; i < 2; i++ { + tc.store.consistencyQueue.process(tc.repl) + } + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35073.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35073.go new file mode 100644 index 00000000000000..f00a7bd46259eb --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35073.go @@ -0,0 +1,124 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "sync/atomic" +) + +func init() { + register("Cockroach35073", Cockroach35073) +} + +type ConsumerStatus_cockroach35073 uint32 + +const ( + NeedMoreRows_cockroach35073 ConsumerStatus_cockroach35073 = iota + DrainRequested_cockroach35073 + ConsumerClosed_cockroach35073 +) + +const rowChannelBufSize_cockroach35073 = 16 +const outboxBufRows_cockroach35073 = 16 + +type rowSourceBase_cockroach35073 struct { + consumerStatus ConsumerStatus_cockroach35073 +} + +func (rb *rowSourceBase_cockroach35073) consumerClosed() { + atomic.StoreUint32((*uint32)(&rb.consumerStatus), uint32(ConsumerClosed_cockroach35073)) +} + +type RowChannelMsg_cockroach35073 int + +type RowChannel_cockroach35073 struct { + rowSourceBase_cockroach35073 + dataChan chan RowChannelMsg_cockroach35073 +} + +func (rc *RowChannel_cockroach35073) ConsumerClosed() { + rc.consumerClosed() + select { + case <-rc.dataChan: + default: + } +} + +func (rc *RowChannel_cockroach35073) Push() ConsumerStatus_cockroach35073 { + consumerStatus := ConsumerStatus_cockroach35073( + atomic.LoadUint32((*uint32)(&rc.consumerStatus))) + switch consumerStatus { + case NeedMoreRows_cockroach35073: + rc.dataChan <- RowChannelMsg_cockroach35073(0) + case DrainRequested_cockroach35073: + case ConsumerClosed_cockroach35073: + } + return consumerStatus +} + +func (rc *RowChannel_cockroach35073) InitWithNumSenders() { + rc.initWithBufSizeAndNumSenders(rowChannelBufSize_cockroach35073) +} + +func (rc *RowChannel_cockroach35073) initWithBufSizeAndNumSenders(chanBufSize int) { + rc.dataChan = make(chan RowChannelMsg_cockroach35073, chanBufSize) +} + +type outbox_cockroach35073 struct { + RowChannel_cockroach35073 +} + +func (m *outbox_cockroach35073) init() { + m.RowChannel_cockroach35073.InitWithNumSenders() +} + +func (m *outbox_cockroach35073) start(wg *sync.WaitGroup) { + if wg != nil { + wg.Add(1) + } + go m.run(wg) +} + +func (m *outbox_cockroach35073) run(wg *sync.WaitGroup) { + if wg != nil { + wg.Done() + } +} + +func Cockroach35073() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + outbox := &outbox_cockroach35073{} + outbox.init() + + var wg sync.WaitGroup + for i := 0; i < outboxBufRows_cockroach35073; i++ { + outbox.Push() + } + + var blockedPusherWg sync.WaitGroup + blockedPusherWg.Add(1) + go func() { + outbox.Push() + blockedPusherWg.Done() + }() + + outbox.start(&wg) + + wg.Wait() + outbox.RowChannel_cockroach35073.Push() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35931.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35931.go new file mode 100644 index 00000000000000..9ddcda1b6242bf --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach35931.go @@ -0,0 +1,135 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Cockroach35931", Cockroach35931) +} + +type RowReceiver_cockroach35931 interface { + Push() +} + +type inboundStreamInfo_cockroach35931 struct { + receiver RowReceiver_cockroach35931 +} + +type RowChannel_cockroach35931 struct { + dataChan chan struct{} +} + +func (rc *RowChannel_cockroach35931) Push() { + // The buffer size can be either 0 or 1 when this function is entered. + // We need context sensitivity or a path-condition on the buffer size + // to find this bug. + rc.dataChan <- struct{}{} +} + +func (rc *RowChannel_cockroach35931) initWithBufSizeAndNumSenders(chanBufSize int) { + rc.dataChan = make(chan struct{}, chanBufSize) +} + +type flowEntry_cockroach35931 struct { + flow *Flow_cockroach35931 + inboundStreams map[int]*inboundStreamInfo_cockroach35931 +} + +type flowRegistry_cockroach35931 struct { + sync.Mutex + flows map[int]*flowEntry_cockroach35931 +} + +func (fr *flowRegistry_cockroach35931) getEntryLocked(id int) *flowEntry_cockroach35931 { + entry, ok := fr.flows[id] + if !ok { + entry = &flowEntry_cockroach35931{} + fr.flows[id] = entry + } + return entry +} + +func (fr *flowRegistry_cockroach35931) cancelPendingStreamsLocked(id int) []RowReceiver_cockroach35931 { + entry := fr.flows[id] + pendingReceivers := make([]RowReceiver_cockroach35931, 0) + for _, is := range entry.inboundStreams { + pendingReceivers = append(pendingReceivers, is.receiver) + } + return pendingReceivers +} + +type Flow_cockroach35931 struct { + id int + flowRegistry *flowRegistry_cockroach35931 + inboundStreams map[int]*inboundStreamInfo_cockroach35931 +} + +func (f *Flow_cockroach35931) cancel() { + f.flowRegistry.Lock() + timedOutReceivers := f.flowRegistry.cancelPendingStreamsLocked(f.id) + f.flowRegistry.Unlock() + + for _, receiver := range timedOutReceivers { + receiver.Push() + } +} + +func (fr *flowRegistry_cockroach35931) RegisterFlow(f *Flow_cockroach35931, inboundStreams map[int]*inboundStreamInfo_cockroach35931) { + entry := fr.getEntryLocked(f.id) + entry.flow = f + entry.inboundStreams = inboundStreams +} + +func makeFlowRegistry_cockroach35931() *flowRegistry_cockroach35931 { + return &flowRegistry_cockroach35931{ + flows: make(map[int]*flowEntry_cockroach35931), + } +} + +func Cockroach35931() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + fr := makeFlowRegistry_cockroach35931() + + left := &RowChannel_cockroach35931{} + left.initWithBufSizeAndNumSenders(1) + right := &RowChannel_cockroach35931{} + right.initWithBufSizeAndNumSenders(1) + + inboundStreams := map[int]*inboundStreamInfo_cockroach35931{ + 0: { + receiver: left, + }, + 1: { + receiver: right, + }, + } + + left.Push() + + flow := &Flow_cockroach35931{ + id: 0, + flowRegistry: fr, + inboundStreams: inboundStreams, + } + + fr.RegisterFlow(flow, inboundStreams) + + flow.cancel() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach3710.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach3710.go new file mode 100644 index 00000000000000..e419cd2fc32019 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach3710.go @@ -0,0 +1,122 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/3710 + * Buggy version: 4afdd4860fd7c3bd9e92489f84a95e5cc7d11a0d + * fix commit-id: cb65190f9caaf464723e7d072b1f1b69a044ef7b + * Flaky: 2/100 + */ + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" + "unsafe" +) + +func init() { + register("Cockroach3710", Cockroach3710) +} + +type Store_cockroach3710 struct { + raftLogQueue *baseQueue + replicas map[int]*Replica_cockroach3710 + + mu struct { + sync.RWMutex + } +} + +func (s *Store_cockroach3710) ForceRaftLogScanAndProcess() { + s.mu.RLock() + runtime.Gosched() + for _, r := range s.replicas { + s.raftLogQueue.MaybeAdd(r) + } + s.mu.RUnlock() +} + +func (s *Store_cockroach3710) RaftStatus() { + s.mu.RLock() + defer s.mu.RUnlock() +} + +func (s *Store_cockroach3710) processRaft() { + go func() { + for { + var replicas []*Replica_cockroach3710 + s.mu.Lock() + for _, r := range s.replicas { + replicas = append(replicas, r) + } + s.mu.Unlock() + break + } + }() +} + +type Replica_cockroach3710 struct { + store *Store_cockroach3710 +} + +type baseQueue struct { + sync.Mutex + impl *raftLogQueue +} + +func (bq *baseQueue) MaybeAdd(repl *Replica_cockroach3710) { + bq.Lock() + defer bq.Unlock() + bq.impl.shouldQueue(repl) +} + +type raftLogQueue struct{} + +func (*raftLogQueue) shouldQueue(r *Replica_cockroach3710) { + getTruncatableIndexes(r) +} + +func getTruncatableIndexes(r *Replica_cockroach3710) { + r.store.RaftStatus() +} + +func NewStore_cockroach3710() *Store_cockroach3710 { + rlq := &raftLogQueue{} + bq := &baseQueue{impl: rlq} + store := &Store_cockroach3710{ + raftLogQueue: bq, + replicas: make(map[int]*Replica_cockroach3710), + } + r1 := &Replica_cockroach3710{store} + r2 := &Replica_cockroach3710{store} + + makeKey := func(r *Replica_cockroach3710) int { + return int((uintptr(unsafe.Pointer(r)) >> 1) % 7) + } + store.replicas[makeKey(r1)] = r1 + store.replicas[makeKey(r2)] = r2 + + return store +} + +func Cockroach3710() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 10000; i++ { + go func() { + store := NewStore_cockroach3710() + go store.ForceRaftLogScanAndProcess() // G1 + go store.processRaft() // G2 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach584.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach584.go new file mode 100644 index 00000000000000..33f7ba7a45ec9a --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach584.go @@ -0,0 +1,62 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Cockroach584", Cockroach584) +} + +type gossip_cockroach584 struct { + mu sync.Mutex // L1 + closed bool +} + +func (g *gossip_cockroach584) bootstrap() { + for { + g.mu.Lock() + if g.closed { + // Missing g.mu.Unlock + break + } + g.mu.Unlock() + } +} + +func (g *gossip_cockroach584) manage() { + for { + g.mu.Lock() + if g.closed { + // Missing g.mu.Unlock + break + } + g.mu.Unlock() + } +} + +func Cockroach584() { + prof := pprof.Lookup("goroutineleak") + defer func() { + for i := 0; i < yieldCount; i++ { + // Yield several times to allow the child goroutine to run. + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + g := &gossip_cockroach584{ + closed: true, + } + go func() { // G1 + g.bootstrap() + g.manage() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach6181.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach6181.go new file mode 100644 index 00000000000000..80f1dd504daae5 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach6181.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/6181 + * Buggy version: c0a232b5521565904b851699853bdbd0c670cf1e + * fix commit-id: d5814e4886a776bf7789b3c51b31f5206480d184 + * Flaky: 57/100 + */ +package main + +import ( + "io" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Cockroach6181", Cockroach6181) +} + +type testDescriptorDB_cockroach6181 struct { + cache *rangeDescriptorCache_cockroach6181 +} + +func initTestDescriptorDB_cockroach6181() *testDescriptorDB_cockroach6181 { + return &testDescriptorDB_cockroach6181{&rangeDescriptorCache_cockroach6181{}} +} + +type rangeDescriptorCache_cockroach6181 struct { + rangeCacheMu sync.RWMutex +} + +func (rdc *rangeDescriptorCache_cockroach6181) LookupRangeDescriptor() { + rdc.rangeCacheMu.RLock() + runtime.Gosched() + io.Discard.Write([]byte(rdc.String())) + rdc.rangeCacheMu.RUnlock() + rdc.rangeCacheMu.Lock() + rdc.rangeCacheMu.Unlock() +} + +func (rdc *rangeDescriptorCache_cockroach6181) String() string { + rdc.rangeCacheMu.RLock() + defer rdc.rangeCacheMu.RUnlock() + return rdc.stringLocked() +} + +func (rdc *rangeDescriptorCache_cockroach6181) stringLocked() string { + return "something here" +} + +func doLookupWithToken_cockroach6181(rc *rangeDescriptorCache_cockroach6181) { + rc.LookupRangeDescriptor() +} + +func testRangeCacheCoalescedRequests_cockroach6181() { + db := initTestDescriptorDB_cockroach6181() + pauseLookupResumeAndAssert := func() { + var wg sync.WaitGroup + for i := 0; i < 3; i++ { + wg.Add(1) + go func() { // G2,G3,... + doLookupWithToken_cockroach6181(db.cache) + wg.Done() + }() + } + wg.Wait() + } + pauseLookupResumeAndAssert() +} + +func Cockroach6181() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go testRangeCacheCoalescedRequests_cockroach6181() // G1 + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach7504.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach7504.go new file mode 100644 index 00000000000000..945308a76f92b1 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach7504.go @@ -0,0 +1,183 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/7504 + * Buggy version: bc963b438cdc3e0ad058a5282358e5aee0595e17 + * fix commit-id: cab761b9f5ee5dee1448bc5d6b1d9f5a0ff0bad5 + * Flaky: 1/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Cockroach7504", Cockroach7504) +} + +func MakeCacheKey_cockroach7504(lease *LeaseState_cockroach7504) int { + return lease.id +} + +type LeaseState_cockroach7504 struct { + mu sync.Mutex // L1 + id int +} +type LeaseSet_cockroach7504 struct { + data []*LeaseState_cockroach7504 +} + +func (l *LeaseSet_cockroach7504) find(id int) *LeaseState_cockroach7504 { + return l.data[id] +} + +func (l *LeaseSet_cockroach7504) remove(s *LeaseState_cockroach7504) { + for i := 0; i < len(l.data); i++ { + if s == l.data[i] { + l.data = append(l.data[:i], l.data[i+1:]...) + break + } + } +} + +type tableState_cockroach7504 struct { + tableNameCache *tableNameCache_cockroach7504 + mu sync.Mutex // L3 + active *LeaseSet_cockroach7504 +} + +func (t *tableState_cockroach7504) release(lease *LeaseState_cockroach7504) { + t.mu.Lock() // L3 + defer t.mu.Unlock() // L3 + + s := t.active.find(MakeCacheKey_cockroach7504(lease)) + s.mu.Lock() // L1 + runtime.Gosched() + defer s.mu.Unlock() // L1 + + t.removeLease(s) +} +func (t *tableState_cockroach7504) removeLease(lease *LeaseState_cockroach7504) { + t.active.remove(lease) + t.tableNameCache.remove(lease) // L1 acquire/release +} + +type tableNameCache_cockroach7504 struct { + mu sync.Mutex // L2 + tables map[int]*LeaseState_cockroach7504 +} + +func (c *tableNameCache_cockroach7504) get(id int) { + c.mu.Lock() // L2 + defer c.mu.Unlock() // L2 + lease, ok := c.tables[id] + if !ok { + return + } + if lease == nil { + panic("nil lease in name cache") + } + lease.mu.Lock() // L1 + defer lease.mu.Unlock() // L1 +} + +func (c *tableNameCache_cockroach7504) remove(lease *LeaseState_cockroach7504) { + c.mu.Lock() // L2 + runtime.Gosched() + defer c.mu.Unlock() // L2 + key := MakeCacheKey_cockroach7504(lease) + existing, ok := c.tables[key] + if !ok { + return + } + if existing == lease { + delete(c.tables, key) + } +} + +type LeaseManager_cockroach7504 struct { + _ [64]byte + tableNames *tableNameCache_cockroach7504 + tables map[int]*tableState_cockroach7504 +} + +func (m *LeaseManager_cockroach7504) AcquireByName(id int) { + m.tableNames.get(id) +} + +func (m *LeaseManager_cockroach7504) findTableState(lease *LeaseState_cockroach7504) *tableState_cockroach7504 { + existing, ok := m.tables[lease.id] + if !ok { + return nil + } + return existing +} + +func (m *LeaseManager_cockroach7504) Release(lease *LeaseState_cockroach7504) { + t := m.findTableState(lease) + t.release(lease) +} +func NewLeaseManager_cockroach7504(tname *tableNameCache_cockroach7504, ts *tableState_cockroach7504) *LeaseManager_cockroach7504 { + mgr := &LeaseManager_cockroach7504{ + tableNames: tname, + tables: make(map[int]*tableState_cockroach7504), + } + mgr.tables[0] = ts + return mgr +} +func NewLeaseSet_cockroach7504(n int) *LeaseSet_cockroach7504 { + lset := &LeaseSet_cockroach7504{} + for i := 0; i < n; i++ { + lease := new(LeaseState_cockroach7504) + lset.data = append(lset.data, lease) + } + return lset +} + +func Cockroach7504() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go func() { + leaseNum := 2 + lset := NewLeaseSet_cockroach7504(leaseNum) + + nc := &tableNameCache_cockroach7504{ + tables: make(map[int]*LeaseState_cockroach7504), + } + for i := 0; i < leaseNum; i++ { + nc.tables[i] = lset.find(i) + } + + ts := &tableState_cockroach7504{ + tableNameCache: nc, + active: lset, + } + + mgr := NewLeaseManager_cockroach7504(nc, ts) + + // G1 + go func() { + // lock L2-L1 + mgr.AcquireByName(0) + }() + + // G2 + go func() { + // lock L1-L2 + mgr.Release(lset.find(0)) + }() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach9935.go b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach9935.go new file mode 100644 index 00000000000000..e143a6670d8ff2 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/cockroach9935.go @@ -0,0 +1,77 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: cockroach + * Issue or PR : https://github.com/cockroachdb/cockroach/pull/9935 + * Buggy version: 4df302cc3f03328395dc3fefbfba58b7718e4f2f + * fix commit-id: ed6a100ba38dd51b0888b9a3d3ac6bdbb26c528c + * Flaky: 100/100 + * Description: This leak is caused by acquiring l.mu.Lock() twice. The fix is + * to release l.mu.Lock() before acquiring l.mu.Lock for the second time. + */ +package main + +import ( + "errors" + "math/rand" + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Cockroach9935", Cockroach9935) +} + +type loggingT_cockroach9935 struct { + mu sync.Mutex +} + +func (l *loggingT_cockroach9935) outputLogEntry() { + l.mu.Lock() + if err := l.createFile(); err != nil { + l.exit(err) + } + l.mu.Unlock() +} + +func (l *loggingT_cockroach9935) createFile() error { + if rand.Intn(8)%4 > 0 { + return errors.New("") + } + return nil +} + +func (l *loggingT_cockroach9935) exit(err error) { + l.mu.Lock() // Blocked forever + defer l.mu.Unlock() +} + +// Example of goroutine leak trace: +// +// G1 +//---------------------------- +// l.outputLogEntry() +// l.mu.Lock() +// l.createFile() +// l.exit() +// l.mu.Lock() +//-----------G1 leaks--------- + +func Cockroach9935() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + l := &loggingT_cockroach9935{} + go l.outputLogEntry() // G1 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd10492.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd10492.go new file mode 100644 index 00000000000000..7d56642d5e9ed9 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd10492.go @@ -0,0 +1,72 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Etcd10492", Etcd10492) +} + +type Checkpointer_etcd10492 func(ctx context.Context) + +type lessor_etcd10492 struct { + mu sync.RWMutex + cp Checkpointer_etcd10492 + checkpointInterval time.Duration +} + +func (le *lessor_etcd10492) Checkpoint() { + le.mu.Lock() // Lock acquired twice here + defer le.mu.Unlock() +} + +func (le *lessor_etcd10492) SetCheckpointer(cp Checkpointer_etcd10492) { + le.mu.Lock() + defer le.mu.Unlock() + + le.cp = cp +} + +func (le *lessor_etcd10492) Renew() { + le.mu.Lock() + unlock := func() { le.mu.Unlock() } + defer func() { unlock() }() + + if le.cp != nil { + le.cp(context.Background()) + } +} + +func Etcd10492() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + go func() { // G1 + le := &lessor_etcd10492{ + checkpointInterval: 0, + } + fakerCheckerpointer_etcd10492 := func(ctx context.Context) { + le.Checkpoint() + } + le.SetCheckpointer(fakerCheckerpointer_etcd10492) + le.mu.Lock() + le.mu.Unlock() + le.Renew() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd5509.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd5509.go new file mode 100644 index 00000000000000..868e926e66949a --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd5509.go @@ -0,0 +1,126 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "io" + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Etcd5509", Etcd5509) +} + +var ErrConnClosed_etcd5509 error + +type Client_etcd5509 struct { + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc +} + +func (c *Client_etcd5509) Close() { + c.mu.Lock() + defer c.mu.Unlock() + if c.cancel == nil { + return + } + c.cancel() + c.cancel = nil + c.mu.Unlock() + c.mu.Lock() +} + +type remoteClient_etcd5509 struct { + client *Client_etcd5509 + mu sync.Mutex +} + +func (r *remoteClient_etcd5509) acquire(ctx context.Context) error { + for { + r.client.mu.RLock() + closed := r.client.cancel == nil + r.mu.Lock() + r.mu.Unlock() + if closed { + return ErrConnClosed_etcd5509 // Missing RUnlock before return + } + r.client.mu.RUnlock() + } +} + +type kv_etcd5509 struct { + rc *remoteClient_etcd5509 +} + +func (kv *kv_etcd5509) Get(ctx context.Context) error { + return kv.Do(ctx) +} + +func (kv *kv_etcd5509) Do(ctx context.Context) error { + for { + err := kv.do(ctx) + if err == nil { + return nil + } + return err + } +} + +func (kv *kv_etcd5509) do(ctx context.Context) error { + err := kv.getRemote(ctx) + return err +} + +func (kv *kv_etcd5509) getRemote(ctx context.Context) error { + return kv.rc.acquire(ctx) +} + +type KV interface { + Get(ctx context.Context) error + Do(ctx context.Context) error +} + +func NewKV_etcd5509(c *Client_etcd5509) KV { + return &kv_etcd5509{rc: &remoteClient_etcd5509{ + client: c, + }} +} + +func Etcd5509() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + go func() { + ctx, _ := context.WithCancel(context.TODO()) + cli := &Client_etcd5509{ + ctx: ctx, + } + kv := NewKV_etcd5509(cli) + donec := make(chan struct{}) + go func() { + defer close(donec) + err := kv.Get(context.TODO()) + if err != nil && err != ErrConnClosed_etcd5509 { + io.Discard.Write([]byte("Expect ErrConnClosed")) + } + }() + + runtime.Gosched() + cli.Close() + + <-donec + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6708.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6708.go new file mode 100644 index 00000000000000..afbbe35104bbb8 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6708.go @@ -0,0 +1,100 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Etcd6708", Etcd6708) +} + +type EndpointSelectionMode_etcd6708 int + +const ( + EndpointSelectionRandom_etcd6708 EndpointSelectionMode_etcd6708 = iota + EndpointSelectionPrioritizeLeader_etcd6708 +) + +type MembersAPI_etcd6708 interface { + Leader(ctx context.Context) +} + +type Client_etcd6708 interface { + Sync(ctx context.Context) + SetEndpoints() + httpClient_etcd6708 +} + +type httpClient_etcd6708 interface { + Do(context.Context) +} + +type httpClusterClient_etcd6708 struct { + sync.RWMutex + selectionMode EndpointSelectionMode_etcd6708 +} + +func (c *httpClusterClient_etcd6708) getLeaderEndpoint() { + mAPI := NewMembersAPI_etcd6708(c) + mAPI.Leader(context.Background()) +} + +func (c *httpClusterClient_etcd6708) SetEndpoints() { + switch c.selectionMode { + case EndpointSelectionRandom_etcd6708: + case EndpointSelectionPrioritizeLeader_etcd6708: + c.getLeaderEndpoint() + } +} + +func (c *httpClusterClient_etcd6708) Do(ctx context.Context) { + c.RLock() + c.RUnlock() +} + +func (c *httpClusterClient_etcd6708) Sync(ctx context.Context) { + c.Lock() + defer c.Unlock() + + c.SetEndpoints() +} + +type httpMembersAPI_etcd6708 struct { + client httpClient_etcd6708 +} + +func (m *httpMembersAPI_etcd6708) Leader(ctx context.Context) { + m.client.Do(ctx) +} + +func NewMembersAPI_etcd6708(c Client_etcd6708) MembersAPI_etcd6708 { + return &httpMembersAPI_etcd6708{ + client: c, + } +} + +func Etcd6708() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + go func() { + hc := &httpClusterClient_etcd6708{ + selectionMode: EndpointSelectionPrioritizeLeader_etcd6708, + } + hc.Sync(context.Background()) + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6857.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6857.go new file mode 100644 index 00000000000000..0798ab23d3b588 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6857.go @@ -0,0 +1,81 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: etcd + * Issue or PR : https://github.com/etcd-io/etcd/pull/6857 + * Buggy version: 7c8f13aed7fe251e7066ed6fc1a090699c2cae0e + * fix commit-id: 7afc490c95789c408fbc256d8e790273d331c984 + * Flaky: 19/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "time" +) + +func init() { + register("Etcd6857", Etcd6857) +} + +type Status_etcd6857 struct{} + +type node_etcd6857 struct { + status chan chan Status_etcd6857 + stop chan struct{} + done chan struct{} +} + +func (n *node_etcd6857) Status() Status_etcd6857 { + c := make(chan Status_etcd6857) + n.status <- c + return <-c +} + +func (n *node_etcd6857) run() { + for { + select { + case c := <-n.status: + c <- Status_etcd6857{} + case <-n.stop: + close(n.done) + return + } + } +} + +func (n *node_etcd6857) Stop() { + select { + case n.stop <- struct{}{}: + case <-n.done: + return + } + <-n.done +} + +func NewNode_etcd6857() *node_etcd6857 { + return &node_etcd6857{ + status: make(chan chan Status_etcd6857), + stop: make(chan struct{}), + done: make(chan struct{}), + } +} + +func Etcd6857() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i <= 100; i++ { + go func() { + n := NewNode_etcd6857() + go n.run() // G1 + go n.Status() // G2 + go n.Stop() // G3 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6873.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6873.go new file mode 100644 index 00000000000000..1846d0f260c2f6 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd6873.go @@ -0,0 +1,98 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: etcd + * Issue or PR : https://github.com/etcd-io/etcd/commit/7618fdd1d642e47cac70c03f637b0fd798a53a6e + * Buggy version: 377f19b0031f9c0aafe2aec28b6f9019311f52f9 + * fix commit-id: 7618fdd1d642e47cac70c03f637b0fd798a53a6e + * Flaky: 9/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Etcd6873", Etcd6873) +} + +type watchBroadcast_etcd6873 struct{} + +type watchBroadcasts_etcd6873 struct { + mu sync.Mutex + updatec chan *watchBroadcast_etcd6873 + donec chan struct{} +} + +func newWatchBroadcasts_etcd6873() *watchBroadcasts_etcd6873 { + wbs := &watchBroadcasts_etcd6873{ + updatec: make(chan *watchBroadcast_etcd6873, 1), + donec: make(chan struct{}), + } + go func() { // G2 + defer close(wbs.donec) + for wb := range wbs.updatec { + wbs.coalesce(wb) + } + }() + return wbs +} + +func (wbs *watchBroadcasts_etcd6873) coalesce(wb *watchBroadcast_etcd6873) { + wbs.mu.Lock() + wbs.mu.Unlock() +} + +func (wbs *watchBroadcasts_etcd6873) stop() { + wbs.mu.Lock() + defer wbs.mu.Unlock() + close(wbs.updatec) + <-wbs.donec +} + +func (wbs *watchBroadcasts_etcd6873) update(wb *watchBroadcast_etcd6873) { + select { + case wbs.updatec <- wb: + default: + } +} + +// Example of goroutine leak trace: +// +// G1 G2 G3 +//--------------------------------------------------------- +// newWatchBroadcasts() +// wbs.update() +// wbs.updatec <- +// return +// <-wbs.updatec +// wbs.coalesce() +// wbs.stop() +// wbs.mu.Lock() +// close(wbs.updatec) +// <-wbs.donec +// wbs.mu.Lock() +//---------------------G2,G3 leak------------------------- +// + +func Etcd6873() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + wbs := newWatchBroadcasts_etcd6873() // G1 + wbs.update(&watchBroadcast_etcd6873{}) + go wbs.stop() // G3 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd7492.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd7492.go new file mode 100644 index 00000000000000..3c8d58a221cac5 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd7492.go @@ -0,0 +1,163 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: etcd + * Issue or PR : https://github.com/etcd-io/etcd/pull/7492 + * Buggy version: 51939650057d602bb5ab090633138fffe36854dc + * fix commit-id: 1b1fabef8ffec606909f01c3983300fff539f214 + * Flaky: 40/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Etcd7492", Etcd7492) +} + +type TokenProvider_etcd7492 interface { + assign() + enable() + disable() +} + +type simpleTokenTTLKeeper_etcd7492 struct { + tokens map[string]time.Time + addSimpleTokenCh chan struct{} + stopCh chan chan struct{} + deleteTokenFunc func(string) +} + +type authStore_etcd7492 struct { + tokenProvider TokenProvider_etcd7492 +} + +func (as *authStore_etcd7492) Authenticate() { + as.tokenProvider.assign() +} + +func NewSimpleTokenTTLKeeper_etcd7492(deletefunc func(string)) *simpleTokenTTLKeeper_etcd7492 { + stk := &simpleTokenTTLKeeper_etcd7492{ + tokens: make(map[string]time.Time), + addSimpleTokenCh: make(chan struct{}, 1), + stopCh: make(chan chan struct{}), + deleteTokenFunc: deletefunc, + } + go stk.run() // G1 + return stk +} + +func (tm *simpleTokenTTLKeeper_etcd7492) run() { + tokenTicker := time.NewTicker(time.Nanosecond) + defer tokenTicker.Stop() + for { + select { + case <-tm.addSimpleTokenCh: + runtime.Gosched() + /// Make tm.tokens not empty is enough + tm.tokens["1"] = time.Now() + case <-tokenTicker.C: + runtime.Gosched() + for t, _ := range tm.tokens { + tm.deleteTokenFunc(t) + delete(tm.tokens, t) + } + case waitCh := <-tm.stopCh: + waitCh <- struct{}{} + return + } + } +} + +func (tm *simpleTokenTTLKeeper_etcd7492) addSimpleToken() { + tm.addSimpleTokenCh <- struct{}{} + runtime.Gosched() +} + +func (tm *simpleTokenTTLKeeper_etcd7492) stop() { + waitCh := make(chan struct{}) + tm.stopCh <- waitCh + <-waitCh + close(tm.stopCh) +} + +type tokenSimple_etcd7492 struct { + simpleTokenKeeper *simpleTokenTTLKeeper_etcd7492 + simpleTokensMu sync.RWMutex +} + +func (t *tokenSimple_etcd7492) assign() { + t.assignSimpleTokenToUser() +} + +func (t *tokenSimple_etcd7492) assignSimpleTokenToUser() { + t.simpleTokensMu.Lock() + runtime.Gosched() + t.simpleTokenKeeper.addSimpleToken() + t.simpleTokensMu.Unlock() +} +func newDeleterFunc(t *tokenSimple_etcd7492) func(string) { + return func(tk string) { + t.simpleTokensMu.Lock() + defer t.simpleTokensMu.Unlock() + } +} + +func (t *tokenSimple_etcd7492) enable() { + t.simpleTokenKeeper = NewSimpleTokenTTLKeeper_etcd7492(newDeleterFunc(t)) +} + +func (t *tokenSimple_etcd7492) disable() { + if t.simpleTokenKeeper != nil { + t.simpleTokenKeeper.stop() + t.simpleTokenKeeper = nil + } + t.simpleTokensMu.Lock() + t.simpleTokensMu.Unlock() +} + +func newTokenProviderSimple_etcd7492() *tokenSimple_etcd7492 { + return &tokenSimple_etcd7492{} +} + +func setupAuthStore_etcd7492() (store *authStore_etcd7492, teardownfunc func()) { + as := &authStore_etcd7492{ + tokenProvider: newTokenProviderSimple_etcd7492(), + } + as.tokenProvider.enable() + tearDown := func() { + as.tokenProvider.disable() + } + return as, tearDown +} + +func Etcd7492() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go func() { + as, tearDown := setupAuthStore_etcd7492() + defer tearDown() + var wg sync.WaitGroup + wg.Add(3) + for i := 0; i < 3; i++ { + go func() { // G2 + as.Authenticate() + defer wg.Done() + }() + } + wg.Wait() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/etcd7902.go b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd7902.go new file mode 100644 index 00000000000000..0a96d7f0472d9e --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/etcd7902.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: etcd + * Issue or PR : https://github.com/coreos/etcd/pull/7902 + * Buggy version: dfdaf082c51ba14861267f632f6af795a27eb4ef + * fix commit-id: 87d99fe0387ee1df1cf1811d88d37331939ef4ae + * Flaky: 100/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Etcd7902", Etcd7902) +} + +type roundClient_etcd7902 struct { + progress int + acquire func() + validate func() + release func() +} + +func runElectionFunc_etcd7902() { + rcs := make([]roundClient_etcd7902, 3) + nextc := make(chan bool) + for i := range rcs { + var rcNextc chan bool + setRcNextc := func() { + rcNextc = nextc + } + rcs[i].acquire = func() {} + rcs[i].validate = func() { + setRcNextc() + } + rcs[i].release = func() { + if i == 0 { // Assume the first roundClient is the leader + close(nextc) + nextc = make(chan bool) + } + <-rcNextc // Follower is blocking here + } + } + doRounds_etcd7902(rcs, 100) +} + +func doRounds_etcd7902(rcs []roundClient_etcd7902, rounds int) { + var mu sync.Mutex + var wg sync.WaitGroup + wg.Add(len(rcs)) + for i := range rcs { + go func(rc *roundClient_etcd7902) { // G2,G3 + defer wg.Done() + for rc.progress < rounds || rounds <= 0 { + rc.acquire() + mu.Lock() + rc.validate() + mu.Unlock() + time.Sleep(10 * time.Millisecond) + rc.progress++ + mu.Lock() + rc.release() + mu.Unlock() + } + }(&rcs[i]) + } + wg.Wait() +} + +func Etcd7902() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go runElectionFunc_etcd7902() // G1 + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1275.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1275.go new file mode 100644 index 00000000000000..ec5491e438f74a --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1275.go @@ -0,0 +1,111 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: grpc-go + * Issue or PR : https://github.com/grpc/grpc-go/pull/1275 + * Buggy version: (missing) + * fix commit-id: 0669f3f89e0330e94bb13fa1ce8cc704aab50c9c + * Flaky: 100/100 + */ +package main + +import ( + "io" + "os" + "runtime/pprof" + "time" +) + +func init() { + register("Grpc1275", Grpc1275) +} + +type recvBuffer_grpc1275 struct { + c chan bool +} + +func (b *recvBuffer_grpc1275) get() <-chan bool { + return b.c +} + +type recvBufferReader_grpc1275 struct { + recv *recvBuffer_grpc1275 +} + +func (r *recvBufferReader_grpc1275) Read(p []byte) (int, error) { + select { + case <-r.recv.get(): + } + return 0, nil +} + +type Stream_grpc1275 struct { + trReader io.Reader +} + +func (s *Stream_grpc1275) Read(p []byte) (int, error) { + return io.ReadFull(s.trReader, p) +} + +type http2Client_grpc1275 struct{} + +func (t *http2Client_grpc1275) CloseStream(s *Stream_grpc1275) { + // It is the client.CloseSream() method called by the + // main goroutine that should send the message, but it + // is not. The patch is to send out this message. +} + +func (t *http2Client_grpc1275) NewStream() *Stream_grpc1275 { + return &Stream_grpc1275{ + trReader: &recvBufferReader_grpc1275{ + recv: &recvBuffer_grpc1275{ + c: make(chan bool), + }, + }, + } +} + +func testInflightStreamClosing_grpc1275() { + client := &http2Client_grpc1275{} + stream := client.NewStream() + donec := make(chan bool) + go func() { // G2 + defer close(donec) + stream.Read([]byte{1}) + }() + + client.CloseStream(stream) + + timeout := time.NewTimer(300 * time.Nanosecond) + select { + case <-donec: + if !timeout.Stop() { + <-timeout.C + } + case <-timeout.C: + } +} + +/// +/// G1 G2 +/// testInflightStreamClosing() +/// stream.Read() +/// io.ReadFull() +/// <- r.recv.get() +/// CloseStream() +/// <- donec +/// ------------G1 timeout, G2 leak--------------------- +/// + +func Grpc1275() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + go func() { + testInflightStreamClosing_grpc1275() // G1 + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1424.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1424.go new file mode 100644 index 00000000000000..777534a788163c --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1424.go @@ -0,0 +1,105 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: grpc-go + * Issue or PR : https://github.com/grpc/grpc-go/pull/1424 + * Buggy version: 39c8c3866d926d95e11c03508bf83d00f2963f91 + * fix commit-id: 64bd0b04a7bb1982078bae6a2ab34c226125fbc1 + * Flaky: 100/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Grpc1424", Grpc1424) +} + +type Balancer_grpc1424 interface { + Notify() <-chan bool +} + +type roundRobin_grpc1424 struct { + mu sync.Mutex + addrCh chan bool +} + +func (rr *roundRobin_grpc1424) Notify() <-chan bool { + return rr.addrCh +} + +type addrConn_grpc1424 struct { + mu sync.Mutex +} + +func (ac *addrConn_grpc1424) tearDown() { + ac.mu.Lock() + defer ac.mu.Unlock() +} + +type dialOption_grpc1424 struct { + balancer Balancer_grpc1424 +} + +type ClientConn_grpc1424 struct { + dopts dialOption_grpc1424 + conns []*addrConn_grpc1424 +} + +func (cc *ClientConn_grpc1424) lbWatcher(doneChan chan bool) { + for addr := range cc.dopts.balancer.Notify() { + if addr { + // nop, make compiler happy + } + var ( + del []*addrConn_grpc1424 + ) + for _, a := range cc.conns { + del = append(del, a) + } + for _, c := range del { + c.tearDown() + } + } +} + +func NewClientConn_grpc1424() *ClientConn_grpc1424 { + cc := &ClientConn_grpc1424{ + dopts: dialOption_grpc1424{ + &roundRobin_grpc1424{addrCh: make(chan bool)}, + }, + } + return cc +} + +func DialContext_grpc1424() { + cc := NewClientConn_grpc1424() + waitC := make(chan error, 1) + go func() { // G2 + defer close(waitC) + ch := cc.dopts.balancer.Notify() + if ch != nil { + doneChan := make(chan bool) + go cc.lbWatcher(doneChan) // G3 + <-doneChan + } + }() + /// close addrCh + close(cc.dopts.balancer.(*roundRobin_grpc1424).addrCh) +} + +func Grpc1424() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + go DialContext_grpc1424() // G1 +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1460.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1460.go new file mode 100644 index 00000000000000..bc658b408d8c6b --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc1460.go @@ -0,0 +1,84 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: grpc + * Issue or PR : https://github.com/grpc/grpc-go/pull/1460 + * Buggy version: 7db1564ba1229bc42919bb1f6d9c4186f3aa8678 + * fix commit-id: e605a1ecf24b634f94f4eefdab10a9ada98b70dd + * Flaky: 100/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Grpc1460", Grpc1460) +} + +type Stream_grpc1460 struct{} + +type http2Client_grpc1460 struct { + mu sync.Mutex + awakenKeepalive chan struct{} + activeStream []*Stream_grpc1460 +} + +func (t *http2Client_grpc1460) keepalive() { + t.mu.Lock() + if len(t.activeStream) < 1 { + <-t.awakenKeepalive + runtime.Gosched() + t.mu.Unlock() + } else { + t.mu.Unlock() + } +} + +func (t *http2Client_grpc1460) NewStream() { + t.mu.Lock() + runtime.Gosched() + t.activeStream = append(t.activeStream, &Stream_grpc1460{}) + if len(t.activeStream) == 1 { + select { + case t.awakenKeepalive <- struct{}{}: + default: + } + } + t.mu.Unlock() +} + +/// +/// G1 G2 +/// client.keepalive() +/// client.NewStream() +/// t.mu.Lock() +/// <-t.awakenKeepalive +/// t.mu.Lock() +/// ---------------G1, G2 deadlock-------------- +/// + +func Grpc1460() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 1000; i++ { + go func() { + client := &http2Client_grpc1460{ + awakenKeepalive: make(chan struct{}), + } + go client.keepalive() //G1 + go client.NewStream() //G2 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc3017.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc3017.go new file mode 100644 index 00000000000000..0523b9509fdcbe --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc3017.go @@ -0,0 +1,123 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +// This test case is a reproduction of grpc/3017. +// +// It is a goroutine leak that also simultaneously engages many GC assists. +// Testing runtime behaviour when pivoting between regular and goroutine leak detection modes. + +func init() { + register("Grpc3017", Grpc3017) +} + +type Address_grpc3017 int +type SubConn_grpc3017 int + +type subConnCacheEntry_grpc3017 struct { + sc SubConn_grpc3017 + cancel func() + abortDeleting bool +} + +type lbCacheClientConn_grpc3017 struct { + mu sync.Mutex // L1 + timeout time.Duration + subConnCache map[Address_grpc3017]*subConnCacheEntry_grpc3017 + subConnToAddr map[SubConn_grpc3017]Address_grpc3017 +} + +func (ccc *lbCacheClientConn_grpc3017) NewSubConn(addrs []Address_grpc3017) SubConn_grpc3017 { + if len(addrs) != 1 { + return SubConn_grpc3017(1) + } + addrWithoutMD := addrs[0] + ccc.mu.Lock() // L1 + defer ccc.mu.Unlock() + if entry, ok := ccc.subConnCache[addrWithoutMD]; ok { + entry.cancel() + delete(ccc.subConnCache, addrWithoutMD) + return entry.sc + } + scNew := SubConn_grpc3017(1) + ccc.subConnToAddr[scNew] = addrWithoutMD + return scNew +} + +func (ccc *lbCacheClientConn_grpc3017) RemoveSubConn(sc SubConn_grpc3017) { + ccc.mu.Lock() // L1 + defer ccc.mu.Unlock() + addr, ok := ccc.subConnToAddr[sc] + if !ok { + return + } + + if entry, ok := ccc.subConnCache[addr]; ok { + if entry.sc != sc { + delete(ccc.subConnToAddr, sc) + } + return + } + + entry := &subConnCacheEntry_grpc3017{ + sc: sc, + } + ccc.subConnCache[addr] = entry + + timer := time.AfterFunc(ccc.timeout, func() { // G3 + runtime.Gosched() + ccc.mu.Lock() // L1 + if entry.abortDeleting { + return // Missing unlock + } + delete(ccc.subConnToAddr, sc) + delete(ccc.subConnCache, addr) + ccc.mu.Unlock() + }) + + entry.cancel = func() { + if !timer.Stop() { + entry.abortDeleting = true + } + } +} + +func Grpc3017() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { //G1 + done := make(chan struct{}) + + ccc := &lbCacheClientConn_grpc3017{ + timeout: time.Nanosecond, + subConnCache: make(map[Address_grpc3017]*subConnCacheEntry_grpc3017), + subConnToAddr: make(map[SubConn_grpc3017]Address_grpc3017), + } + + sc := ccc.NewSubConn([]Address_grpc3017{Address_grpc3017(1)}) + go func() { // G2 + for i := 0; i < 10000; i++ { + ccc.RemoveSubConn(sc) + sc = ccc.NewSubConn([]Address_grpc3017{Address_grpc3017(1)}) + } + close(done) + }() + <-done + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc660.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc660.go new file mode 100644 index 00000000000000..5f6201ec8062d9 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc660.go @@ -0,0 +1,65 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: grpc-go + * Issue or PR : https://github.com/grpc/grpc-go/pull/660 + * Buggy version: db85417dd0de6cc6f583672c6175a7237e5b5dd2 + * fix commit-id: ceacfbcbc1514e4e677932fd55938ac455d182fb + * Flaky: 100/100 + */ +package main + +import ( + "math/rand" + "os" + "runtime" + "runtime/pprof" +) + +func init() { + register("Grpc660", Grpc660) +} + +type benchmarkClient_grpc660 struct { + stop chan bool +} + +func (bc *benchmarkClient_grpc660) doCloseLoopUnary() { + for { + done := make(chan bool) + go func() { // G2 + if rand.Intn(10) > 7 { + done <- false + return + } + done <- true + }() + select { + case <-bc.stop: + return + case <-done: + } + } +} + +func Grpc660() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + bc := &benchmarkClient_grpc660{ + stop: make(chan bool), + } + go bc.doCloseLoopUnary() // G1 + go func() { // helper goroutine + bc.stop <- true + }() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc795.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc795.go new file mode 100644 index 00000000000000..72005cc844e225 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc795.go @@ -0,0 +1,74 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Grpc795", Grpc795) +} + +type Server_grpc795 struct { + mu sync.Mutex + drain bool +} + +func (s *Server_grpc795) GracefulStop() { + s.mu.Lock() + if s.drain { + s.mu.Lock() + return + } + s.drain = true + s.mu.Unlock() +} +func (s *Server_grpc795) Serve() { + s.mu.Lock() + s.mu.Unlock() +} + +func NewServer_grpc795() *Server_grpc795 { + return &Server_grpc795{} +} + +type test_grpc795 struct { + srv *Server_grpc795 +} + +func (te *test_grpc795) startServer() { + s := NewServer_grpc795() + te.srv = s + go s.Serve() +} + +func newTest_grpc795() *test_grpc795 { + return &test_grpc795{} +} + +func testServerGracefulStopIdempotent_grpc795() { + te := newTest_grpc795() + + te.startServer() + + for i := 0; i < 3; i++ { + te.srv.GracefulStop() + } +} + +func Grpc795() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go testServerGracefulStopIdempotent_grpc795() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/grpc862.go b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc862.go new file mode 100644 index 00000000000000..188b3b88ba5f8a --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/grpc862.go @@ -0,0 +1,105 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: grpc-go + * Issue or PR : https://github.com/grpc/grpc-go/pull/862 + * Buggy version: d8f4ebe77f6b7b6403d7f98626de8a534f9b93a7 + * fix commit-id: dd5645bebff44f6b88780bb949022a09eadd7dae + * Flaky: 100/100 + */ +package main + +import ( + "context" + "os" + "runtime" + "runtime/pprof" + "time" +) + +func init() { + register("Grpc862", Grpc862) +} + +type ClientConn_grpc862 struct { + ctx context.Context + cancel context.CancelFunc + conns []*addrConn_grpc862 +} + +func (cc *ClientConn_grpc862) Close() { + cc.cancel() + conns := cc.conns + cc.conns = nil + for _, ac := range conns { + ac.tearDown() + } +} + +func (cc *ClientConn_grpc862) resetAddrConn() { + ac := &addrConn_grpc862{ + cc: cc, + } + cc.conns = append(cc.conns, ac) + ac.ctx, ac.cancel = context.WithCancel(cc.ctx) + ac.resetTransport() +} + +type addrConn_grpc862 struct { + cc *ClientConn_grpc862 + ctx context.Context + cancel context.CancelFunc +} + +func (ac *addrConn_grpc862) resetTransport() { + for retries := 1; ; retries++ { + _ = 2 * time.Nanosecond * time.Duration(retries) + timeout := 10 * time.Nanosecond + _, cancel := context.WithTimeout(ac.ctx, timeout) + _ = time.Now() + cancel() + <-ac.ctx.Done() + return + } +} + +func (ac *addrConn_grpc862) tearDown() { + ac.cancel() +} + +func DialContext_grpc862(ctx context.Context) (conn *ClientConn_grpc862) { + cc := &ClientConn_grpc862{} + cc.ctx, cc.cancel = context.WithCancel(context.Background()) + defer func() { + select { + case <-ctx.Done(): + if conn != nil { + conn.Close() + } + conn = nil + default: + } + }() + go func() { // G2 + cc.resetAddrConn() + }() + return conn +} + +func Grpc862() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + ctx, cancel := context.WithCancel(context.Background()) + go DialContext_grpc862(ctx) // G1 + go cancel() // helper goroutine + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/hugo3251.go b/src/runtime/testdata/testgoroutineleakprofile/goker/hugo3251.go new file mode 100644 index 00000000000000..3804692a8bf6bd --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/hugo3251.go @@ -0,0 +1,81 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Hugo3251", Hugo3251) +} + +type remoteLock_hugo3251 struct { + sync.RWMutex // L1 + m map[string]*sync.Mutex // L2 +} + +func (l *remoteLock_hugo3251) URLLock(url string) { + l.Lock() // L1 + if _, ok := l.m[url]; !ok { + l.m[url] = &sync.Mutex{} + } + l.m[url].Lock() // L2 + runtime.Gosched() + l.Unlock() // L1 + // runtime.Gosched() +} + +func (l *remoteLock_hugo3251) URLUnlock(url string) { + l.RLock() // L1 + defer l.RUnlock() // L1 + if um, ok := l.m[url]; ok { + um.Unlock() // L2 + } +} + +func resGetRemote_hugo3251(remoteURLLock *remoteLock_hugo3251, url string) error { + remoteURLLock.URLLock(url) + defer func() { remoteURLLock.URLUnlock(url) }() + + return nil +} + +func Hugo3251() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(time.Second) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 11; i++ { + go func() { // G1 + url := "http://Foo.Bar/foo_Bar-Foo" + remoteURLLock := &remoteLock_hugo3251{m: make(map[string]*sync.Mutex)} + for range []bool{false, true} { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(gor int) { // G2 + defer wg.Done() + for j := 0; j < 200; j++ { + err := resGetRemote_hugo3251(remoteURLLock, url) + if err != nil { + fmt.Errorf("Error getting resource content: %s", err) + } + time.Sleep(300 * time.Nanosecond) + } + }(i) + } + wg.Wait() + } + }() + } +} \ No newline at end of file diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/hugo5379.go b/src/runtime/testdata/testgoroutineleakprofile/goker/hugo5379.go new file mode 100644 index 00000000000000..6a1bbe9a3f7767 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/hugo5379.go @@ -0,0 +1,317 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "log" + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Hugo5379", Hugo5379) +} + +type shortcodeHandler_hugo5379 struct { + p *PageWithoutContent_hugo5379 + contentShortcodes map[int]func() error + contentShortcodesDelta map[int]func() error + init sync.Once // O1 +} + +func (s *shortcodeHandler_hugo5379) executeShortcodesForDelta(p *PageWithoutContent_hugo5379) error { + for k, _ := range s.contentShortcodesDelta { + render := s.contentShortcodesDelta[k] + if err := render(); err != nil { + continue + } + } + return nil +} + +func (s *shortcodeHandler_hugo5379) updateDelta() { + s.init.Do(func() { + s.contentShortcodes = createShortcodeRenderers_hugo5379(s.p.withoutContent()) + }) + + delta := make(map[int]func() error) + + for k, v := range s.contentShortcodes { + if _, ok := delta[k]; !ok { + delta[k] = v + } + } + + s.contentShortcodesDelta = delta +} + +type Page_hugo5379 struct { + *pageInit_hugo5379 + *pageContentInit_hugo5379 + pageWithoutContent *PageWithoutContent_hugo5379 + contentInit sync.Once // O2 + contentInitMu sync.Mutex // L1 + shortcodeState *shortcodeHandler_hugo5379 +} + +func (p *Page_hugo5379) WordCount() { + p.initContentPlainAndMeta() +} + +func (p *Page_hugo5379) initContentPlainAndMeta() { + p.initContent() + p.initPlain(true) +} + +func (p *Page_hugo5379) initPlain(lock bool) { + p.plainInit.Do(func() { + if lock { + /// Double locking here. + p.contentInitMu.Lock() + defer p.contentInitMu.Unlock() + } + }) +} + +func (p *Page_hugo5379) withoutContent() *PageWithoutContent_hugo5379 { + p.pageInit_hugo5379.withoutContentInit.Do(func() { + p.pageWithoutContent = &PageWithoutContent_hugo5379{Page_hugo5379: p} + }) + return p.pageWithoutContent +} + +func (p *Page_hugo5379) prepareForRender() error { + var err error + if err = handleShortcodes_hugo5379(p.withoutContent()); err != nil { + return err + } + return nil +} + +func (p *Page_hugo5379) setContentInit() { + p.shortcodeState.updateDelta() +} + +func (p *Page_hugo5379) initContent() { + p.contentInit.Do(func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + c := make(chan error, 1) + + go func() { // G2 + var err error + p.contentInitMu.Lock() // first lock here + defer p.contentInitMu.Unlock() + + err = p.prepareForRender() + if err != nil { + c <- err + return + } + c <- err + }() + + select { + case <-ctx.Done(): + case <-c: + } + }) +} + +type PageWithoutContent_hugo5379 struct { + *Page_hugo5379 +} + +type pageInit_hugo5379 struct { + withoutContentInit sync.Once +} + +type pageContentInit_hugo5379 struct { + contentInit sync.Once // O3 + plainInit sync.Once // O4 +} + +type HugoSites_hugo5379 struct { + Sites []*Site_hugo5379 +} + +func (h *HugoSites_hugo5379) render() { + for _, s := range h.Sites { + for _, s2 := range h.Sites { + s2.preparePagesForRender() + } + s.renderPages() + } +} + +func (h *HugoSites_hugo5379) Build() { + h.render() +} + +type Pages_hugo5379 []*Page_hugo5379 + +type PageCollections_hugo5379 struct { + Pages Pages_hugo5379 +} + +type Site_hugo5379 struct { + *PageCollections_hugo5379 +} + +func (s *Site_hugo5379) preparePagesForRender() { + for _, p := range s.Pages { + p.setContentInit() + } +} + +func (s *Site_hugo5379) renderForLayouts() { + /// Omit reflections + for _, p := range s.Pages { + p.WordCount() + } +} + +func (s *Site_hugo5379) renderAndWritePage() { + s.renderForLayouts() +} + +func (s *Site_hugo5379) renderPages() { + numWorkers := 2 + wg := &sync.WaitGroup{} + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go pageRenderer_hugo5379(s, wg) // G3 + } + + wg.Wait() +} + +type sitesBuilder_hugo5379 struct { + H *HugoSites_hugo5379 +} + +func (s *sitesBuilder_hugo5379) Build() *sitesBuilder_hugo5379 { + return s.build() +} + +func (s *sitesBuilder_hugo5379) build() *sitesBuilder_hugo5379 { + s.H.Build() + return s +} + +func (s *sitesBuilder_hugo5379) CreateSitesE() error { + sites, err := NewHugoSites_hugo5379() + if err != nil { + return err + } + s.H = sites + return nil +} + +func (s *sitesBuilder_hugo5379) CreateSites() *sitesBuilder_hugo5379 { + if err := s.CreateSitesE(); err != nil { + log.Fatalf("Failed to create sites: %s", err) + } + return s +} + +func newHugoSites_hugo5379(sites ...*Site_hugo5379) (*HugoSites_hugo5379, error) { + h := &HugoSites_hugo5379{Sites: sites} + return h, nil +} + +func newSite_hugo5379() *Site_hugo5379 { + c := &PageCollections_hugo5379{} + s := &Site_hugo5379{ + PageCollections_hugo5379: c, + } + return s +} + +func createSitesFromConfig_hugo5379() []*Site_hugo5379 { + var ( + sites []*Site_hugo5379 + ) + + var s *Site_hugo5379 = newSite_hugo5379() + sites = append(sites, s) + return sites +} + +func NewHugoSites_hugo5379() (*HugoSites_hugo5379, error) { + sites := createSitesFromConfig_hugo5379() + return newHugoSites_hugo5379(sites...) +} + +func prepareShortcodeForPage_hugo5379(p *PageWithoutContent_hugo5379) map[int]func() error { + m := make(map[int]func() error) + m[0] = func() error { + return renderShortcode_hugo5379(p) + } + return m +} + +func renderShortcode_hugo5379(p *PageWithoutContent_hugo5379) error { + return renderShortcodeWithPage_hugo5379(p) +} + +func renderShortcodeWithPage_hugo5379(p *PageWithoutContent_hugo5379) error { + /// Omit reflections + p.WordCount() + return nil +} + +func createShortcodeRenderers_hugo5379(p *PageWithoutContent_hugo5379) map[int]func() error { + return prepareShortcodeForPage_hugo5379(p) +} + +func newShortcodeHandler_hugo5379(p *Page_hugo5379) *shortcodeHandler_hugo5379 { + return &shortcodeHandler_hugo5379{ + p: p.withoutContent(), + contentShortcodes: make(map[int]func() error), + contentShortcodesDelta: make(map[int]func() error), + } +} + +func handleShortcodes_hugo5379(p *PageWithoutContent_hugo5379) error { + return p.shortcodeState.executeShortcodesForDelta(p) +} + +func pageRenderer_hugo5379(s *Site_hugo5379, wg *sync.WaitGroup) { + defer wg.Done() + s.renderAndWritePage() +} + +func Hugo5379() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { // G1 + b := &sitesBuilder_hugo5379{} + s := b.CreateSites() + for _, site := range s.H.Sites { + p := &Page_hugo5379{ + pageInit_hugo5379: &pageInit_hugo5379{}, + pageContentInit_hugo5379: &pageContentInit_hugo5379{}, + pageWithoutContent: &PageWithoutContent_hugo5379{}, + contentInit: sync.Once{}, + contentInitMu: sync.Mutex{}, + shortcodeState: nil, + } + p.shortcodeState = newShortcodeHandler_hugo5379(p) + site.Pages = append(site.Pages, p) + } + s.Build() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/istio16224.go b/src/runtime/testdata/testgoroutineleakprofile/goker/istio16224.go new file mode 100644 index 00000000000000..839051cc64a18d --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/istio16224.go @@ -0,0 +1,129 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Istio16224", Istio16224) +} + +type ConfigStoreCache_istio16224 interface { + RegisterEventHandler(handler func()) + Run() +} + +type Event_istio16224 int + +type Handler_istio16224 func(Event_istio16224) + +type configstoreMonitor_istio16224 struct { + handlers []Handler_istio16224 + eventCh chan Event_istio16224 +} + +func (m *configstoreMonitor_istio16224) Run(stop <-chan struct{}) { + for { + select { + case <-stop: + // This bug is not descibed, but is a true positive (in our eyes) + // In a real run main exits when the goro is blocked here. + if _, ok := <-m.eventCh; ok { + close(m.eventCh) + } + return + case ce, ok := <-m.eventCh: + if ok { + m.processConfigEvent(ce) + } + } + } +} + +func (m *configstoreMonitor_istio16224) processConfigEvent(ce Event_istio16224) { + m.applyHandlers(ce) +} + +func (m *configstoreMonitor_istio16224) AppendEventHandler(h Handler_istio16224) { + m.handlers = append(m.handlers, h) +} + +func (m *configstoreMonitor_istio16224) applyHandlers(e Event_istio16224) { + for _, f := range m.handlers { + f(e) + } +} +func (m *configstoreMonitor_istio16224) ScheduleProcessEvent(configEvent Event_istio16224) { + m.eventCh <- configEvent +} + +type Monitor_istio16224 interface { + Run(<-chan struct{}) + AppendEventHandler(Handler_istio16224) + ScheduleProcessEvent(Event_istio16224) +} + +type controller_istio16224 struct { + monitor Monitor_istio16224 +} + +func (c *controller_istio16224) RegisterEventHandler(f func(Event_istio16224)) { + c.monitor.AppendEventHandler(f) +} + +func (c *controller_istio16224) Run(stop <-chan struct{}) { + c.monitor.Run(stop) +} + +func (c *controller_istio16224) Create() { + c.monitor.ScheduleProcessEvent(Event_istio16224(0)) +} + +func NewMonitor_istio16224() Monitor_istio16224 { + return NewBufferedMonitor_istio16224() +} + +func NewBufferedMonitor_istio16224() Monitor_istio16224 { + return &configstoreMonitor_istio16224{ + eventCh: make(chan Event_istio16224), + } +} + +func Istio16224() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + controller := &controller_istio16224{monitor: NewMonitor_istio16224()} + done := make(chan bool) + lock := sync.Mutex{} + controller.RegisterEventHandler(func(event Event_istio16224) { + lock.Lock() + defer lock.Unlock() + done <- true + }) + + stop := make(chan struct{}) + go controller.Run(stop) + + controller.Create() + + lock.Lock() // blocks + lock.Unlock() + <-done + + close(stop) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/istio17860.go b/src/runtime/testdata/testgoroutineleakprofile/goker/istio17860.go new file mode 100644 index 00000000000000..aa8317c6d5f41b --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/istio17860.go @@ -0,0 +1,144 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "os" + "runtime/pprof" + + "sync" + "time" +) + +func init() { + register("Istio17860", Istio17860) +} + +type Proxy_istio17860 interface { + IsLive() bool +} + +type TestProxy_istio17860 struct { + live func() bool +} + +func (tp TestProxy_istio17860) IsLive() bool { + if tp.live == nil { + return true + } + return tp.live() +} + +type Agent_istio17860 interface { + Run(ctx context.Context) + Restart() +} + +type exitStatus_istio17860 int + +type agent_istio17860 struct { + proxy Proxy_istio17860 + mu *sync.Mutex + statusCh chan exitStatus_istio17860 + currentEpoch int + activeEpochs map[int]struct{} +} + +func (a *agent_istio17860) Run(ctx context.Context) { + for { + select { + case status := <-a.statusCh: + a.mu.Lock() + delete(a.activeEpochs, int(status)) + active := len(a.activeEpochs) + a.mu.Unlock() + if active == 0 { + return + } + case <-ctx.Done(): + return + } + } +} + +func (a *agent_istio17860) Restart() { + a.mu.Lock() + defer a.mu.Unlock() + + a.waitUntilLive() + a.currentEpoch++ + a.activeEpochs[a.currentEpoch] = struct{}{} + + go a.runWait(a.currentEpoch) +} + +func (a *agent_istio17860) runWait(epoch int) { + a.statusCh <- exitStatus_istio17860(epoch) +} + +func (a *agent_istio17860) waitUntilLive() { + if len(a.activeEpochs) == 0 { + return + } + + interval := time.NewTicker(30 * time.Nanosecond) + timer := time.NewTimer(100 * time.Nanosecond) + defer func() { + interval.Stop() + timer.Stop() + }() + + if a.proxy.IsLive() { + return + } + + for { + select { + case <-timer.C: + return + case <-interval.C: + if a.proxy.IsLive() { + return + } + } + } +} + +func NewAgent_istio17860(proxy Proxy_istio17860) Agent_istio17860 { + return &agent_istio17860{ + proxy: proxy, + mu: &sync.Mutex{}, + statusCh: make(chan exitStatus_istio17860), + activeEpochs: make(map[int]struct{}), + } +} + +func Istio17860() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + neverLive := func() bool { + return false + } + + a := NewAgent_istio17860(TestProxy_istio17860{live: neverLive}) + go func() { a.Run(ctx) }() + + a.Restart() + go a.Restart() + + time.Sleep(200 * time.Nanosecond) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/istio18454.go b/src/runtime/testdata/testgoroutineleakprofile/goker/istio18454.go new file mode 100644 index 00000000000000..b410c490322440 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/istio18454.go @@ -0,0 +1,154 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "os" + "runtime/pprof" + + "sync" + "time" +) + +func init() { + register("Istio18454", Istio18454) +} + +const eventChCap_istio18454 = 1024 + +type Worker_istio18454 struct { + ctx context.Context + ctxCancel context.CancelFunc +} + +func (w *Worker_istio18454) Start(setupFn func(), runFn func(c context.Context)) { + if setupFn != nil { + setupFn() + } + go func() { + runFn(w.ctx) + }() +} + +func (w *Worker_istio18454) Stop() { + w.ctxCancel() +} + +type Strategy_istio18454 struct { + timer *time.Timer + timerFrequency time.Duration + stateLock sync.Mutex + resetChan chan struct{} + worker *Worker_istio18454 + startTimerFn func() +} + +func (s *Strategy_istio18454) OnChange() { + s.stateLock.Lock() + if s.timer != nil { + s.stateLock.Unlock() + s.resetChan <- struct{}{} + return + } + s.startTimerFn() + s.stateLock.Unlock() +} + +func (s *Strategy_istio18454) startTimer() { + s.timer = time.NewTimer(s.timerFrequency) + eventLoop := func(ctx context.Context) { + for { + select { + case <-s.timer.C: + case <-s.resetChan: + if !s.timer.Stop() { + <-s.timer.C + } + s.timer.Reset(s.timerFrequency) + case <-ctx.Done(): + s.timer.Stop() + return + } + } + } + s.worker.Start(nil, eventLoop) +} + +func (s *Strategy_istio18454) Close() { + s.worker.Stop() +} + +type Event_istio18454 int + +type Processor_istio18454 struct { + stateStrategy *Strategy_istio18454 + worker *Worker_istio18454 + eventCh chan Event_istio18454 +} + +func (p *Processor_istio18454) processEvent() { + p.stateStrategy.OnChange() +} + +func (p *Processor_istio18454) Start() { + setupFn := func() { + for i := 0; i < eventChCap_istio18454; i++ { + p.eventCh <- Event_istio18454(0) + } + } + runFn := func(ctx context.Context) { + defer func() { + p.stateStrategy.Close() + }() + for { + select { + case <-ctx.Done(): + return + case <-p.eventCh: + p.processEvent() + } + } + } + p.worker.Start(setupFn, runFn) +} + +func (p *Processor_istio18454) Stop() { + p.worker.Stop() +} + +func NewWorker_istio18454() *Worker_istio18454 { + worker := &Worker_istio18454{} + worker.ctx, worker.ctxCancel = context.WithCancel(context.Background()) + return worker +} + +func Istio18454() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + stateStrategy := &Strategy_istio18454{ + timerFrequency: time.Nanosecond, + resetChan: make(chan struct{}, 1), + worker: NewWorker_istio18454(), + } + stateStrategy.startTimerFn = stateStrategy.startTimer + + p := &Processor_istio18454{ + stateStrategy: stateStrategy, + worker: NewWorker_istio18454(), + eventCh: make(chan Event_istio18454, eventChCap_istio18454), + } + + p.Start() + defer p.Stop() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes10182.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes10182.go new file mode 100644 index 00000000000000..0eca5f41fbfab4 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes10182.go @@ -0,0 +1,95 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/10182 + * Buggy version: 4b990d128a17eea9058d28a3b3688ab8abafbd94 + * fix commit-id: 64ad3e17ad15cd0f9a4fd86706eec1c572033254 + * Flaky: 15/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes10182", Kubernetes10182) +} + +type statusManager_kubernetes10182 struct { + podStatusesLock sync.RWMutex + podStatusChannel chan bool +} + +func (s *statusManager_kubernetes10182) Start() { + go func() { + for i := 0; i < 2; i++ { + s.syncBatch() + } + }() +} + +func (s *statusManager_kubernetes10182) syncBatch() { + runtime.Gosched() + <-s.podStatusChannel + s.DeletePodStatus() +} + +func (s *statusManager_kubernetes10182) DeletePodStatus() { + s.podStatusesLock.Lock() + defer s.podStatusesLock.Unlock() +} + +func (s *statusManager_kubernetes10182) SetPodStatus() { + s.podStatusesLock.Lock() + defer s.podStatusesLock.Unlock() + s.podStatusChannel <- true +} + +func NewStatusManager_kubernetes10182() *statusManager_kubernetes10182 { + return &statusManager_kubernetes10182{ + podStatusChannel: make(chan bool), + } +} + +// Example of deadlock trace: +// +// G1 G2 G3 +// -------------------------------------------------------------------------------- +// s.Start() +// s.syncBatch() +// s.SetPodStatus() +// <-s.podStatusChannel +// s.podStatusesLock.Lock() +// s.podStatusChannel <- true +// s.podStatusesLock.Unlock() +// return +// s.DeletePodStatus() +// s.podStatusesLock.Lock() +// s.podStatusChannel <- true +// s.podStatusesLock.Lock() +// -----------------------------------G1,G3 leak------------------------------------- + +func Kubernetes10182() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 1000; i++ { + go func() { + s := NewStatusManager_kubernetes10182() + go s.Start() + go s.SetPodStatus() + go s.SetPodStatus() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes11298.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes11298.go new file mode 100644 index 00000000000000..36405f240a6612 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes11298.go @@ -0,0 +1,118 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes11298", Kubernetes11298) +} + +type Signal_kubernetes11298 <-chan struct{} + +func After_kubernetes11298(f func()) Signal_kubernetes11298 { + ch := make(chan struct{}) + go func() { + defer close(ch) + if f != nil { + f() + } + }() + return Signal_kubernetes11298(ch) +} + +func Until_kubernetes11298(f func(), period time.Duration, stopCh <-chan struct{}) { + if f == nil { + return + } + for { + select { + case <-stopCh: + return + default: + } + f() + select { + case <-stopCh: + case <-time.After(period): + } + } + +} + +type notifier_kubernetes11298 struct { + lock sync.Mutex + cond *sync.Cond +} + +// abort will be closed no matter what +func (n *notifier_kubernetes11298) serviceLoop(abort <-chan struct{}) { + n.lock.Lock() + defer n.lock.Unlock() + for { + select { + case <-abort: + return + default: + ch := After_kubernetes11298(func() { + n.cond.Wait() + }) + select { + case <-abort: + n.cond.Signal() + <-ch + return + case <-ch: + } + } + } +} + +// abort will be closed no matter what +func Notify_kubernetes11298(abort <-chan struct{}) { + n := ¬ifier_kubernetes11298{} + n.cond = sync.NewCond(&n.lock) + finished := After_kubernetes11298(func() { + Until_kubernetes11298(func() { + for { + select { + case <-abort: + return + default: + func() { + n.lock.Lock() + defer n.lock.Unlock() + n.cond.Signal() + }() + } + } + }, 0, abort) + }) + Until_kubernetes11298(func() { n.serviceLoop(finished) }, 0, abort) +} +func Kubernetes11298() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 1000; i++ { + go func() { + done := make(chan struct{}) + notifyDone := After_kubernetes11298(func() { Notify_kubernetes11298(done) }) + go func() { + defer close(done) + time.Sleep(300 * time.Nanosecond) + }() + <-notifyDone + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes13135.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes13135.go new file mode 100644 index 00000000000000..f6aa8b9ddfe178 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes13135.go @@ -0,0 +1,166 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/13135 + * Buggy version: 6ced66249d4fd2a81e86b4a71d8df0139fe5ceae + * fix commit-id: a12b7edc42c5c06a2e7d9f381975658692951d5a + * Flaky: 93/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes13135", Kubernetes13135) +} + +var ( + StopChannel_kubernetes13135 chan struct{} +) + +func Util_kubernetes13135(f func(), period time.Duration, stopCh <-chan struct{}) { + for { + select { + case <-stopCh: + return + default: + } + func() { + f() + }() + time.Sleep(period) + } +} + +type Store_kubernetes13135 interface { + Add(obj interface{}) + Replace(obj interface{}) +} + +type Reflector_kubernetes13135 struct { + store Store_kubernetes13135 +} + +func (r *Reflector_kubernetes13135) ListAndWatch(stopCh <-chan struct{}) error { + r.syncWith() + return nil +} + +func NewReflector_kubernetes13135(store Store_kubernetes13135) *Reflector_kubernetes13135 { + return &Reflector_kubernetes13135{ + store: store, + } +} + +func (r *Reflector_kubernetes13135) syncWith() { + r.store.Replace(nil) +} + +type Cacher_kubernetes13135 struct { + sync.Mutex + initialized sync.WaitGroup + initOnce sync.Once + watchCache *WatchCache_kubernetes13135 + reflector *Reflector_kubernetes13135 +} + +func (c *Cacher_kubernetes13135) processEvent() { + c.Lock() + defer c.Unlock() +} + +func (c *Cacher_kubernetes13135) startCaching(stopChannel <-chan struct{}) { + c.Lock() + for { + err := c.reflector.ListAndWatch(stopChannel) + if err == nil { + break + } + } +} + +type WatchCache_kubernetes13135 struct { + sync.RWMutex + onReplace func() + onEvent func() +} + +func (w *WatchCache_kubernetes13135) SetOnEvent(onEvent func()) { + w.Lock() + defer w.Unlock() + w.onEvent = onEvent +} + +func (w *WatchCache_kubernetes13135) SetOnReplace(onReplace func()) { + w.Lock() + defer w.Unlock() + w.onReplace = onReplace +} + +func (w *WatchCache_kubernetes13135) processEvent() { + w.Lock() + defer w.Unlock() + if w.onEvent != nil { + w.onEvent() + } +} + +func (w *WatchCache_kubernetes13135) Add(obj interface{}) { + w.processEvent() +} + +func (w *WatchCache_kubernetes13135) Replace(obj interface{}) { + w.Lock() + defer w.Unlock() + if w.onReplace != nil { + w.onReplace() + } +} + +func NewCacher_kubernetes13135(stopCh <-chan struct{}) *Cacher_kubernetes13135 { + watchCache := &WatchCache_kubernetes13135{} + cacher := &Cacher_kubernetes13135{ + initialized: sync.WaitGroup{}, + watchCache: watchCache, + reflector: NewReflector_kubernetes13135(watchCache), + } + cacher.initialized.Add(1) + watchCache.SetOnReplace(func() { + cacher.initOnce.Do(func() { cacher.initialized.Done() }) + cacher.Unlock() + }) + watchCache.SetOnEvent(cacher.processEvent) + go Util_kubernetes13135(func() { cacher.startCaching(stopCh) }, 0, stopCh) // G2 + cacher.initialized.Wait() + return cacher +} + +func Kubernetes13135() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + StopChannel_kubernetes13135 = make(chan struct{}) + for i := 0; i < 50; i++ { + go func() { + // Should create a local channel. Using a single global channel + // concurrently will cause a deadlock which does not actually exist + // in the original microbenchmark. + StopChannel_kubernetes13135 := make(chan struct{}) + + c := NewCacher_kubernetes13135(StopChannel_kubernetes13135) // G1 + go c.watchCache.Add(nil) // G3 + go close(StopChannel_kubernetes13135) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes1321.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes1321.go new file mode 100644 index 00000000000000..6c0139a9d24fcb --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes1321.go @@ -0,0 +1,100 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/1321 + * Buggy version: 9cd0fc70f1ca852c903b18b0933991036b3b2fa1 + * fix commit-id: 435e0b73bb99862f9dedf56a50260ff3dfef14ff + * Flaky: 1/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes1321", Kubernetes1321) +} + +type muxWatcher_kubernetes1321 struct { + result chan struct{} + m *Mux_kubernetes1321 + id int64 +} + +func (mw *muxWatcher_kubernetes1321) Stop() { + mw.m.stopWatching(mw.id) +} + +type Mux_kubernetes1321 struct { + lock sync.Mutex + watchers map[int64]*muxWatcher_kubernetes1321 +} + +func NewMux_kubernetes1321() *Mux_kubernetes1321 { + m := &Mux_kubernetes1321{ + watchers: map[int64]*muxWatcher_kubernetes1321{}, + } + go m.loop() // G2 + return m +} + +func (m *Mux_kubernetes1321) Watch() *muxWatcher_kubernetes1321 { + mw := &muxWatcher_kubernetes1321{ + result: make(chan struct{}), + m: m, + id: int64(len(m.watchers)), + } + m.watchers[mw.id] = mw + runtime.Gosched() + return mw +} + +func (m *Mux_kubernetes1321) loop() { + for i := 0; i < 100; i++ { + m.distribute() + } +} + +func (m *Mux_kubernetes1321) distribute() { + m.lock.Lock() + defer m.lock.Unlock() + for _, w := range m.watchers { + w.result <- struct{}{} + runtime.Gosched() + } +} + +func (m *Mux_kubernetes1321) stopWatching(id int64) { + m.lock.Lock() + defer m.lock.Unlock() + w, ok := m.watchers[id] + if !ok { + return + } + delete(m.watchers, id) + close(w.result) +} + +func testMuxWatcherClose_kubernetes1321() { + m := NewMux_kubernetes1321() + m.watchers[m.Watch().id].Stop() +} + +func Kubernetes1321() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 1000; i++ { + go testMuxWatcherClose_kubernetes1321() // G1 + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes25331.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes25331.go new file mode 100644 index 00000000000000..323cb236c0687a --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes25331.go @@ -0,0 +1,72 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/25331 + * Buggy version: 5dd087040bb13434f1ddf2f0693d0203c30f28cb + * fix commit-id: 97f4647dc3d8cf46c2b66b89a31c758a6edfb57c + * Flaky: 100/100 + */ +package main + +import ( + "context" + "errors" + "os" + "runtime" + "runtime/pprof" +) + +func init() { + register("Kubernetes25331", Kubernetes25331) +} + +type watchChan_kubernetes25331 struct { + ctx context.Context + cancel context.CancelFunc + resultChan chan bool + errChan chan error +} + +func (wc *watchChan_kubernetes25331) Stop() { + wc.errChan <- errors.New("Error") + wc.cancel() +} + +func (wc *watchChan_kubernetes25331) run() { + select { + case err := <-wc.errChan: + errResult := len(err.Error()) != 0 + wc.cancel() // Removed in fix + wc.resultChan <- errResult + case <-wc.ctx.Done(): + } +} + +func NewWatchChan_kubernetes25331() *watchChan_kubernetes25331 { + ctx, cancel := context.WithCancel(context.Background()) + return &watchChan_kubernetes25331{ + ctx: ctx, + cancel: cancel, + resultChan: make(chan bool), + errChan: make(chan error), + } +} + +func Kubernetes25331() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + wc := NewWatchChan_kubernetes25331() + go wc.run() // G1 + go wc.Stop() // G2 + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes26980.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes26980.go new file mode 100644 index 00000000000000..38e53cf4ad7218 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes26980.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes26980", Kubernetes26980) +} + +type processorListener_kubernetes26980 struct { + lock sync.RWMutex + cond sync.Cond + + pendingNotifications []interface{} +} + +func (p *processorListener_kubernetes26980) add(notification interface{}) { + p.lock.Lock() + defer p.lock.Unlock() + + p.pendingNotifications = append(p.pendingNotifications, notification) + p.cond.Broadcast() +} + +func (p *processorListener_kubernetes26980) pop(stopCh <-chan struct{}) { + p.lock.Lock() + runtime.Gosched() + defer p.lock.Unlock() + for { + for len(p.pendingNotifications) == 0 { + select { + case <-stopCh: + return + default: + } + p.cond.Wait() + } + select { + case <-stopCh: + return + } + } +} + +func newProcessListener_kubernetes26980() *processorListener_kubernetes26980 { + ret := &processorListener_kubernetes26980{ + pendingNotifications: []interface{}{}, + } + ret.cond.L = &ret.lock + return ret +} +func Kubernetes26980() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 3000; i++ { + go func() { + pl := newProcessListener_kubernetes26980() + stopCh := make(chan struct{}) + defer close(stopCh) + pl.add(1) + runtime.Gosched() + go pl.pop(stopCh) + + resultCh := make(chan struct{}) + go func() { + pl.lock.Lock() + close(resultCh) + }() + runtime.Gosched() + <-resultCh + pl.lock.Unlock() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes30872.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes30872.go new file mode 100644 index 00000000000000..00cdcf2b678692 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes30872.go @@ -0,0 +1,223 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes30872", Kubernetes30872) +} + +type PopProcessFunc_kubernetes30872 func() + +type ProcessFunc_kubernetes30872 func() + +func Util_kubernetes30872(f func(), stopCh <-chan struct{}) { + JitterUntil_kubernetes30872(f, stopCh) +} + +func JitterUntil_kubernetes30872(f func(), stopCh <-chan struct{}) { + for { + select { + case <-stopCh: + return + default: + } + func() { + f() + }() + } +} + +type Queue_kubernetes30872 interface { + HasSynced() + Pop(PopProcessFunc_kubernetes30872) +} + +type Config_kubernetes30872 struct { + Queue Queue_kubernetes30872 + Process ProcessFunc_kubernetes30872 +} + +type Controller_kubernetes30872 struct { + config Config_kubernetes30872 +} + +func (c *Controller_kubernetes30872) Run(stopCh <-chan struct{}) { + Util_kubernetes30872(c.processLoop, stopCh) +} + +func (c *Controller_kubernetes30872) HasSynced() { + c.config.Queue.HasSynced() +} + +func (c *Controller_kubernetes30872) processLoop() { + c.config.Queue.Pop(PopProcessFunc_kubernetes30872(c.config.Process)) +} + +type ControllerInterface_kubernetes30872 interface { + Run(<-chan struct{}) + HasSynced() +} + +type ResourceEventHandler_kubernetes30872 interface { + OnAdd() +} + +type ResourceEventHandlerFuncs_kubernetes30872 struct { + AddFunc func() +} + +func (r ResourceEventHandlerFuncs_kubernetes30872) OnAdd() { + if r.AddFunc != nil { + r.AddFunc() + } +} + +type informer_kubernetes30872 struct { + controller ControllerInterface_kubernetes30872 + + stopChan chan struct{} +} + +type federatedInformerImpl_kubernetes30872 struct { + sync.Mutex + clusterInformer informer_kubernetes30872 +} + +func (f *federatedInformerImpl_kubernetes30872) ClustersSynced() { + f.Lock() // L1 + defer f.Unlock() + f.clusterInformer.controller.HasSynced() +} + +func (f *federatedInformerImpl_kubernetes30872) addCluster() { + f.Lock() // L1 + defer f.Unlock() +} + +func (f *federatedInformerImpl_kubernetes30872) Start() { + f.Lock() // L1 + defer f.Unlock() + + f.clusterInformer.stopChan = make(chan struct{}) + go f.clusterInformer.controller.Run(f.clusterInformer.stopChan) // G2 + runtime.Gosched() +} + +func (f *federatedInformerImpl_kubernetes30872) Stop() { + f.Lock() // L1 + defer f.Unlock() + close(f.clusterInformer.stopChan) +} + +type DelayingDeliverer_kubernetes30872 struct{} + +func (d *DelayingDeliverer_kubernetes30872) StartWithHandler(handler func()) { + go func() { // G4 + handler() + }() +} + +type FederationView_kubernetes30872 interface { + ClustersSynced() +} + +type FederatedInformer_kubernetes30872 interface { + FederationView_kubernetes30872 + Start() + Stop() +} + +type NamespaceController_kubernetes30872 struct { + namespaceDeliverer *DelayingDeliverer_kubernetes30872 + namespaceFederatedInformer FederatedInformer_kubernetes30872 +} + +func (nc *NamespaceController_kubernetes30872) isSynced() { + nc.namespaceFederatedInformer.ClustersSynced() +} + +func (nc *NamespaceController_kubernetes30872) reconcileNamespace() { + nc.isSynced() +} + +func (nc *NamespaceController_kubernetes30872) Run(stopChan <-chan struct{}) { + nc.namespaceFederatedInformer.Start() + go func() { // G3 + <-stopChan + nc.namespaceFederatedInformer.Stop() + }() + nc.namespaceDeliverer.StartWithHandler(func() { + nc.reconcileNamespace() + }) +} + +type DeltaFIFO_kubernetes30872 struct { + lock sync.RWMutex +} + +func (f *DeltaFIFO_kubernetes30872) HasSynced() { + f.lock.Lock() // L2 + defer f.lock.Unlock() +} + +func (f *DeltaFIFO_kubernetes30872) Pop(process PopProcessFunc_kubernetes30872) { + f.lock.Lock() // L2 + defer f.lock.Unlock() + process() +} + +func NewFederatedInformer_kubernetes30872() FederatedInformer_kubernetes30872 { + federatedInformer := &federatedInformerImpl_kubernetes30872{} + federatedInformer.clusterInformer.controller = NewInformer_kubernetes30872( + ResourceEventHandlerFuncs_kubernetes30872{ + AddFunc: func() { + federatedInformer.addCluster() + }, + }) + return federatedInformer +} + +func NewInformer_kubernetes30872(h ResourceEventHandler_kubernetes30872) *Controller_kubernetes30872 { + fifo := &DeltaFIFO_kubernetes30872{} + cfg := &Config_kubernetes30872{ + Queue: fifo, + Process: func() { + h.OnAdd() + }, + } + return &Controller_kubernetes30872{config: *cfg} +} + +func NewNamespaceController_kubernetes30872() *NamespaceController_kubernetes30872 { + nc := &NamespaceController_kubernetes30872{} + nc.namespaceDeliverer = &DelayingDeliverer_kubernetes30872{} + nc.namespaceFederatedInformer = NewFederatedInformer_kubernetes30872() + return nc +} + +func Kubernetes30872() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { // G1 + namespaceController := NewNamespaceController_kubernetes30872() + stop := make(chan struct{}) + namespaceController.Run(stop) + close(stop) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes38669.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes38669.go new file mode 100644 index 00000000000000..27020d580493bf --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes38669.go @@ -0,0 +1,83 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Kubernetes38669", Kubernetes38669) +} + +type Event_kubernetes38669 int +type watchCacheEvent_kubernetes38669 int + +type cacheWatcher_kubernetes38669 struct { + sync.Mutex + input chan watchCacheEvent_kubernetes38669 + result chan Event_kubernetes38669 + stopped bool +} + +func (c *cacheWatcher_kubernetes38669) process(initEvents []watchCacheEvent_kubernetes38669) { + for _, event := range initEvents { + c.sendWatchCacheEvent(&event) + } + defer close(c.result) + defer c.Stop() + for { + _, ok := <-c.input + if !ok { + return + } + } +} + +func (c *cacheWatcher_kubernetes38669) sendWatchCacheEvent(event *watchCacheEvent_kubernetes38669) { + c.result <- Event_kubernetes38669(*event) +} + +func (c *cacheWatcher_kubernetes38669) Stop() { + c.stop() +} + +func (c *cacheWatcher_kubernetes38669) stop() { + c.Lock() + defer c.Unlock() + if !c.stopped { + c.stopped = true + close(c.input) + } +} + +func newCacheWatcher_kubernetes38669(chanSize int, initEvents []watchCacheEvent_kubernetes38669) *cacheWatcher_kubernetes38669 { + watcher := &cacheWatcher_kubernetes38669{ + input: make(chan watchCacheEvent_kubernetes38669, chanSize), + result: make(chan Event_kubernetes38669, chanSize), + stopped: false, + } + go watcher.process(initEvents) + return watcher +} + +func Kubernetes38669() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + initEvents := []watchCacheEvent_kubernetes38669{1, 2} + w := newCacheWatcher_kubernetes38669(0, initEvents) + w.Stop() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes5316.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes5316.go new file mode 100644 index 00000000000000..fd51484a0f773b --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes5316.go @@ -0,0 +1,68 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/5316 + * Buggy version: c868b0bbf09128960bc7c4ada1a77347a464d876 + * fix commit-id: cc3a433a7abc89d2f766d4c87eaae9448e3dc091 + * Flaky: 100/100 + */ + +package main + +import ( + "errors" + "math/rand" + "os" + "runtime" + "runtime/pprof" + "time" +) + +func init() { + register("Kubernetes5316", Kubernetes5316) +} + +func finishRequest_kubernetes5316(timeout time.Duration, fn func() error) { + ch := make(chan bool) + errCh := make(chan error) + go func() { // G2 + if err := fn(); err != nil { + errCh <- err + } else { + ch <- true + } + }() + + select { + case <-ch: + case <-errCh: + case <-time.After(timeout): + } +} + +func Kubernetes5316() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Wait a bit because the child goroutine relies on timed operations. + time.Sleep(100 * time.Millisecond) + + // Yield several times to allow the child goroutine to run + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + fn := func() error { + time.Sleep(2 * time.Millisecond) + if rand.Intn(10) > 5 { + return errors.New("Error") + } + return nil + } + go finishRequest_kubernetes5316(time.Microsecond, fn) // G1 + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes58107.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes58107.go new file mode 100644 index 00000000000000..0ca707e981693a --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes58107.go @@ -0,0 +1,108 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Tag: Reproduce misbehavior + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/58107 + * Buggy version: 2f17d782eb2772d6401da7ddced9ac90656a7a79 + * fix commit-id: 010a127314a935d8d038f8dd4559fc5b249813e4 + * Flaky: 53/100 + */ + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes58107", Kubernetes58107) +} + +type RateLimitingInterface_kubernetes58107 interface { + Get() + Put() +} + +type Type_kubernetes58107 struct { + cond *sync.Cond +} + +func (q *Type_kubernetes58107) Get() { + q.cond.L.Lock() + defer q.cond.L.Unlock() + q.cond.Wait() +} + +func (q *Type_kubernetes58107) Put() { + q.cond.Signal() +} + +type ResourceQuotaController_kubernetes58107 struct { + workerLock sync.RWMutex + queue RateLimitingInterface_kubernetes58107 + missingUsageQueue RateLimitingInterface_kubernetes58107 +} + +func (rq *ResourceQuotaController_kubernetes58107) worker(queue RateLimitingInterface_kubernetes58107, _ string) { + workFunc := func() bool { + rq.workerLock.RLock() + defer rq.workerLock.RUnlock() + queue.Get() + return true + } + for { + if quit := workFunc(); quit { + return + } + } +} + +func (rq *ResourceQuotaController_kubernetes58107) Run() { + go rq.worker(rq.queue, "G1") // G3 + go rq.worker(rq.missingUsageQueue, "G2") // G4 +} + +func (rq *ResourceQuotaController_kubernetes58107) Sync() { + for i := 0; i < 100000; i++ { + rq.workerLock.Lock() + runtime.Gosched() + rq.workerLock.Unlock() + } +} + +func (rq *ResourceQuotaController_kubernetes58107) HelperSignals() { + for i := 0; i < 100000; i++ { + rq.queue.Put() + rq.missingUsageQueue.Put() + } +} + +func startResourceQuotaController_kubernetes58107() { + resourceQuotaController := &ResourceQuotaController_kubernetes58107{ + queue: &Type_kubernetes58107{sync.NewCond(&sync.Mutex{})}, + missingUsageQueue: &Type_kubernetes58107{sync.NewCond(&sync.Mutex{})}, + } + + go resourceQuotaController.Run() // G2 + go resourceQuotaController.Sync() // G5 + resourceQuotaController.HelperSignals() +} + +func Kubernetes58107() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(1000 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 1000; i++ { + go startResourceQuotaController_kubernetes58107() // G1 + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes62464.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes62464.go new file mode 100644 index 00000000000000..0d07ebc4a9c451 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes62464.go @@ -0,0 +1,120 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/62464 + * Buggy version: a048ca888ad27367b1a7b7377c67658920adbf5d + * fix commit-id: c1b19fce903675b82e9fdd1befcc5f5d658bfe78 + * Flaky: 8/100 + */ + +package main + +import ( + "math/rand" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes62464", Kubernetes62464) +} + +type State_kubernetes62464 interface { + GetCPUSetOrDefault() + GetCPUSet() bool + GetDefaultCPUSet() + SetDefaultCPUSet() +} + +type stateMemory_kubernetes62464 struct { + sync.RWMutex +} + +func (s *stateMemory_kubernetes62464) GetCPUSetOrDefault() { + s.RLock() + defer s.RUnlock() + if ok := s.GetCPUSet(); ok { + return + } + s.GetDefaultCPUSet() +} + +func (s *stateMemory_kubernetes62464) GetCPUSet() bool { + runtime.Gosched() + s.RLock() + defer s.RUnlock() + + if rand.Intn(10) > 5 { + return true + } + return false +} + +func (s *stateMemory_kubernetes62464) GetDefaultCPUSet() { + s.RLock() + defer s.RUnlock() +} + +func (s *stateMemory_kubernetes62464) SetDefaultCPUSet() { + s.Lock() + runtime.Gosched() + defer s.Unlock() +} + +type staticPolicy_kubernetes62464 struct{} + +func (p *staticPolicy_kubernetes62464) RemoveContainer(s State_kubernetes62464) { + s.GetDefaultCPUSet() + s.SetDefaultCPUSet() +} + +type manager_kubernetes62464 struct { + state *stateMemory_kubernetes62464 +} + +func (m *manager_kubernetes62464) reconcileState() { + m.state.GetCPUSetOrDefault() +} + +func NewPolicyAndManager_kubernetes62464() (*staticPolicy_kubernetes62464, *manager_kubernetes62464) { + s := &stateMemory_kubernetes62464{} + m := &manager_kubernetes62464{s} + p := &staticPolicy_kubernetes62464{} + return p, m +} + +/// +/// G1 G2 +/// m.reconcileState() +/// m.state.GetCPUSetOrDefault() +/// s.RLock() +/// s.GetCPUSet() +/// p.RemoveContainer() +/// s.GetDefaultCPUSet() +/// s.SetDefaultCPUSet() +/// s.Lock() +/// s.RLock() +/// ---------------------G1,G2 deadlock--------------------- +/// + +func Kubernetes62464() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 1000; i++ { + go func() { + p, m := NewPolicyAndManager_kubernetes62464() + go m.reconcileState() + go p.RemoveContainer(m.state) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes6632.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes6632.go new file mode 100644 index 00000000000000..a3af3b24ae81ac --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes6632.go @@ -0,0 +1,86 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: kubernetes + * Issue or PR : https://github.com/kubernetes/kubernetes/pull/6632 + * Buggy version: e597b41d939573502c8dda1dde7bf3439325fb5d + * fix commit-id: 82afb7ab1fe12cf2efceede2322d082eaf5d5adc + * Flaky: 4/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Kubernetes6632", Kubernetes6632) +} + +type Connection_kubernetes6632 struct { + closeChan chan bool +} + +type idleAwareFramer_kubernetes6632 struct { + resetChan chan bool + writeLock sync.Mutex + conn *Connection_kubernetes6632 +} + +func (i *idleAwareFramer_kubernetes6632) monitor() { + var resetChan = i.resetChan +Loop: + for { + select { + case <-i.conn.closeChan: + i.writeLock.Lock() + close(resetChan) + i.resetChan = nil + i.writeLock.Unlock() + break Loop + } + } +} + +func (i *idleAwareFramer_kubernetes6632) WriteFrame() { + i.writeLock.Lock() + defer i.writeLock.Unlock() + if i.resetChan == nil { + return + } + i.resetChan <- true +} + +func NewIdleAwareFramer_kubernetes6632() *idleAwareFramer_kubernetes6632 { + return &idleAwareFramer_kubernetes6632{ + resetChan: make(chan bool), + conn: &Connection_kubernetes6632{ + closeChan: make(chan bool), + }, + } +} + +func Kubernetes6632() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + i := NewIdleAwareFramer_kubernetes6632() + + go func() { // helper goroutine + i.conn.closeChan <- true + }() + go i.monitor() // G1 + go i.WriteFrame() // G2 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes70277.go b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes70277.go new file mode 100644 index 00000000000000..ae02ec83040b99 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/kubernetes70277.go @@ -0,0 +1,97 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime/pprof" + "time" +) + +func init() { + register("Kubernetes70277", Kubernetes70277) +} + +type WaitFunc_kubernetes70277 func(done <-chan struct{}) <-chan struct{} + +type ConditionFunc_kubernetes70277 func() (done bool, err error) + +func WaitFor_kubernetes70277(wait WaitFunc_kubernetes70277, fn ConditionFunc_kubernetes70277, done <-chan struct{}) error { + c := wait(done) + for { + _, open := <-c + ok, err := fn() + if err != nil { + return err + } + if ok { + return nil + } + if !open { + break + } + } + return nil +} + +func poller_kubernetes70277(interval, timeout time.Duration) WaitFunc_kubernetes70277 { + return WaitFunc_kubernetes70277(func(done <-chan struct{}) <-chan struct{} { + ch := make(chan struct{}) + go func() { + defer close(ch) + + tick := time.NewTicker(interval) + defer tick.Stop() + + var after <-chan time.Time + if timeout != 0 { + timer := time.NewTimer(timeout) + after = timer.C + defer timer.Stop() + } + for { + select { + case <-tick.C: + select { + case ch <- struct{}{}: + default: + } + case <-after: + return + case <-done: + return + } + } + }() + + return ch + }) +} + +func Kubernetes70277() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 1000; i++ { + go func() { + stopCh := make(chan struct{}) + defer close(stopCh) + waitFunc := poller_kubernetes70277(time.Millisecond, 80*time.Millisecond) + var doneCh <-chan struct{} + + WaitFor_kubernetes70277(func(done <-chan struct{}) <-chan struct{} { + doneCh = done + return waitFunc(done) + }, func() (bool, error) { + time.Sleep(10 * time.Millisecond) + return true, nil + }, stopCh) + + <-doneCh // block here + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/main.go b/src/runtime/testdata/testgoroutineleakprofile/goker/main.go new file mode 100644 index 00000000000000..5787c1e2b2bb90 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/main.go @@ -0,0 +1,39 @@ +// 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 + +import "os" + +// The number of times the main (profiling) goroutine should yield +// in order to allow the leaking goroutines to get stuck. +const yieldCount = 10 + +var cmds = map[string]func(){} + +func register(name string, f func()) { + if cmds[name] != nil { + panic("duplicate registration: " + name) + } + cmds[name] = f +} + +func registerInit(name string, f func()) { + if len(os.Args) >= 2 && os.Args[1] == name { + f() + } +} + +func main() { + if len(os.Args) < 2 { + println("usage: " + os.Args[0] + " name-of-test") + return + } + f := cmds[os.Args[1]] + if f == nil { + println("unknown function: " + os.Args[1]) + return + } + f() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby17176.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby17176.go new file mode 100644 index 00000000000000..884d077550f6b8 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby17176.go @@ -0,0 +1,68 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/17176 + * Buggy version: d295dc66521e2734390473ec1f1da8a73ad3288a + * fix commit-id: 2f16895ee94848e2d8ad72bc01968b4c88d84cb8 + * Flaky: 100/100 + */ +package main + +import ( + "errors" + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby17176", Moby17176) +} + +type DeviceSet_moby17176 struct { + sync.Mutex + nrDeletedDevices int +} + +func (devices *DeviceSet_moby17176) cleanupDeletedDevices() error { + devices.Lock() + if devices.nrDeletedDevices == 0 { + /// Missing devices.Unlock() + return nil + } + devices.Unlock() + return errors.New("Error") +} + +func testDevmapperLockReleasedDeviceDeletion_moby17176() { + ds := &DeviceSet_moby17176{ + nrDeletedDevices: 0, + } + ds.cleanupDeletedDevices() + doneChan := make(chan bool) + go func() { + ds.Lock() + defer ds.Unlock() + doneChan <- true + }() + + select { + case <-time.After(time.Millisecond): + case <-doneChan: + } +} +func Moby17176() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go testDevmapperLockReleasedDeviceDeletion_moby17176() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby21233.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby21233.go new file mode 100644 index 00000000000000..36017a9488cc06 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby21233.go @@ -0,0 +1,146 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/21233 + * Buggy version: cc12d2bfaae135e63b1f962ad80e6943dd995337 + * fix commit-id: 2f4aa9658408ac72a598363c6e22eadf93dbb8a7 + * Flaky:100/100 + */ +package main + +import ( + "math/rand" + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby21233", Moby21233) +} + +type Progress_moby21233 struct{} + +type Output_moby21233 interface { + WriteProgress(Progress_moby21233) error +} + +type chanOutput_moby21233 chan<- Progress_moby21233 + +type TransferManager_moby21233 struct { + mu sync.Mutex +} + +type Transfer_moby21233 struct { + mu sync.Mutex +} + +type Watcher_moby21233 struct { + signalChan chan struct{} + releaseChan chan struct{} + running chan struct{} +} + +func ChanOutput_moby21233(progressChan chan<- Progress_moby21233) Output_moby21233 { + return chanOutput_moby21233(progressChan) +} +func (out chanOutput_moby21233) WriteProgress(p Progress_moby21233) error { + out <- p + return nil +} +func NewTransferManager_moby21233() *TransferManager_moby21233 { + return &TransferManager_moby21233{} +} +func NewTransfer_moby21233() *Transfer_moby21233 { + return &Transfer_moby21233{} +} +func (t *Transfer_moby21233) Release(watcher *Watcher_moby21233) { + t.mu.Lock() + t.mu.Unlock() + close(watcher.releaseChan) + <-watcher.running +} +func (t *Transfer_moby21233) Watch(progressOutput Output_moby21233) *Watcher_moby21233 { + t.mu.Lock() + defer t.mu.Unlock() + lastProgress := Progress_moby21233{} + w := &Watcher_moby21233{ + releaseChan: make(chan struct{}), + signalChan: make(chan struct{}), + running: make(chan struct{}), + } + go func() { // G2 + defer func() { + close(w.running) + }() + done := false + for { + t.mu.Lock() + t.mu.Unlock() + if rand.Int31n(2) >= 1 { + progressOutput.WriteProgress(lastProgress) + } + if done { + return + } + select { + case <-w.signalChan: + case <-w.releaseChan: + done = true + } + } + }() + return w +} +func (tm *TransferManager_moby21233) Transfer(progressOutput Output_moby21233) (*Transfer_moby21233, *Watcher_moby21233) { + tm.mu.Lock() + defer tm.mu.Unlock() + t := NewTransfer_moby21233() + return t, t.Watch(progressOutput) +} + +func testTransfer_moby21233() { // G1 + tm := NewTransferManager_moby21233() + progressChan := make(chan Progress_moby21233) + progressDone := make(chan struct{}) + go func() { // G3 + time.Sleep(1 * time.Millisecond) + for p := range progressChan { /// Chan consumer + if rand.Int31n(2) >= 1 { + return + } + _ = p + } + close(progressDone) + }() + time.Sleep(1 * time.Millisecond) + ids := []string{"id1", "id2", "id3"} + xrefs := make([]*Transfer_moby21233, len(ids)) + watchers := make([]*Watcher_moby21233, len(ids)) + for i := range ids { + xrefs[i], watchers[i] = tm.Transfer(ChanOutput_moby21233(progressChan)) /// Chan producer + time.Sleep(2 * time.Millisecond) + } + + for i := range xrefs { + xrefs[i].Release(watchers[i]) + } + + close(progressChan) + <-progressDone +} + +func Moby21233() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go testTransfer_moby21233() // G1 + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby25384.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby25384.go new file mode 100644 index 00000000000000..d653731f6caea8 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby25384.go @@ -0,0 +1,59 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/25384 + * Buggy version: 58befe3081726ef74ea09198cd9488fb42c51f51 + * fix commit-id: 42360d164b9f25fb4b150ef066fcf57fa39559a7 + * Flaky: 100/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Moby25348", Moby25348) +} + +type plugin_moby25348 struct{} + +type Manager_moby25348 struct { + plugins []*plugin_moby25348 +} + +func (pm *Manager_moby25348) init() { + var group sync.WaitGroup + group.Add(len(pm.plugins)) + for _, p := range pm.plugins { + go func(p *plugin_moby25348) { + defer group.Done() + }(p) + group.Wait() // Block here + } +} + +func Moby25348() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { + p1 := &plugin_moby25348{} + p2 := &plugin_moby25348{} + pm := &Manager_moby25348{ + plugins: []*plugin_moby25348{p1, p2}, + } + go pm.init() + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby27782.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby27782.go new file mode 100644 index 00000000000000..7b3398fd381210 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby27782.go @@ -0,0 +1,242 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/27782 + * Buggy version: 18768fdc2e76ec6c600c8ab57d2d487ee7877794 + * fix commit-id: a69a59ffc7e3d028a72d1195c2c1535f447eaa84 + * Flaky: 2/100 + */ +package main + +import ( + "errors" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby27782", Moby27782) +} + +type Event_moby27782 struct { + Op Op_moby27782 +} + +type Op_moby27782 uint32 + +const ( + Create_moby27782 Op_moby27782 = 1 << iota + Write_moby27782 + Remove_moby27782 + Rename_moby27782 + Chmod_moby27782 +) + +func newEvent(op Op_moby27782) Event_moby27782 { + return Event_moby27782{op} +} + +func (e *Event_moby27782) ignoreLinux(w *Watcher_moby27782) bool { + if e.Op != Write_moby27782 { + w.mu.Lock() + defer w.mu.Unlock() + w.cv.Broadcast() + return true + } + runtime.Gosched() + return false +} + +type Watcher_moby27782 struct { + Events chan Event_moby27782 + mu sync.Mutex // L1 + cv *sync.Cond // C1 + done chan struct{} +} + +func NewWatcher_moby27782() *Watcher_moby27782 { + w := &Watcher_moby27782{ + Events: make(chan Event_moby27782), + done: make(chan struct{}), + } + w.cv = sync.NewCond(&w.mu) + go w.readEvents() // G3 + return w +} + +func (w *Watcher_moby27782) readEvents() { + defer close(w.Events) + for { + if w.isClosed() { + return + } + event := newEvent(Write_moby27782) // MODIFY event + if !event.ignoreLinux(w) { + runtime.Gosched() + select { + case w.Events <- event: + case <-w.done: + return + } + } + } +} + +func (w *Watcher_moby27782) isClosed() bool { + select { + case <-w.done: + return true + default: + return false + } +} + +func (w *Watcher_moby27782) Close() { + if w.isClosed() { + return + } + close(w.done) +} + +func (w *Watcher_moby27782) Remove() { + w.mu.Lock() + defer w.mu.Unlock() + exists := true + for exists { + w.cv.Wait() + runtime.Gosched() + } +} + +type FileWatcher_moby27782 interface { + Events() <-chan Event_moby27782 + Remove() + Close() +} + +func New_moby27782() FileWatcher_moby27782 { + return NewEventWatcher_moby27782() +} + +func NewEventWatcher_moby27782() FileWatcher_moby27782 { + return &fsNotifyWatcher_moby27782{NewWatcher_moby27782()} +} + +type fsNotifyWatcher_moby27782 struct { + *Watcher_moby27782 +} + +func (w *fsNotifyWatcher_moby27782) Events() <-chan Event_moby27782 { + return w.Watcher_moby27782.Events +} + +func watchFile_moby27782() FileWatcher_moby27782 { + fileWatcher := New_moby27782() + return fileWatcher +} + +type LogWatcher_moby27782 struct { + closeOnce sync.Once + closeNotifier chan struct{} +} + +func (w *LogWatcher_moby27782) Close() { + w.closeOnce.Do(func() { + close(w.closeNotifier) + }) +} + +func (w *LogWatcher_moby27782) WatchClose() <-chan struct{} { + return w.closeNotifier +} + +func NewLogWatcher_moby27782() *LogWatcher_moby27782 { + return &LogWatcher_moby27782{ + closeNotifier: make(chan struct{}), + } +} + +func followLogs_moby27782(logWatcher *LogWatcher_moby27782) { + fileWatcher := watchFile_moby27782() + defer func() { + fileWatcher.Close() + }() + waitRead := func() { + runtime.Gosched() + select { + case <-fileWatcher.Events(): + case <-logWatcher.WatchClose(): + fileWatcher.Remove() + return + } + } + handleDecodeErr := func() { + waitRead() + } + handleDecodeErr() +} + +type Container_moby27782 struct { + LogDriver *JSONFileLogger_moby27782 +} + +func (container *Container_moby27782) InitializeStdio() { + if err := container.startLogging(); err != nil { + container.Reset() + } +} + +func (container *Container_moby27782) startLogging() error { + l := &JSONFileLogger_moby27782{ + readers: make(map[*LogWatcher_moby27782]struct{}), + } + container.LogDriver = l + l.ReadLogs() + return errors.New("Some error") +} + +func (container *Container_moby27782) Reset() { + if container.LogDriver != nil { + container.LogDriver.Close() + } +} + +type JSONFileLogger_moby27782 struct { + readers map[*LogWatcher_moby27782]struct{} +} + +func (l *JSONFileLogger_moby27782) ReadLogs() *LogWatcher_moby27782 { + logWatcher := NewLogWatcher_moby27782() + go l.readLogs(logWatcher) // G2 + return logWatcher +} + +func (l *JSONFileLogger_moby27782) readLogs(logWatcher *LogWatcher_moby27782) { + l.readers[logWatcher] = struct{}{} + followLogs_moby27782(logWatcher) +} + +func (l *JSONFileLogger_moby27782) Close() { + for r := range l.readers { + r.Close() + delete(l.readers, r) + } +} + +func Moby27782() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 10000; i++ { + go (&Container_moby27782{}).InitializeStdio() // G1 + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby28462.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby28462.go new file mode 100644 index 00000000000000..56467e0b56f7cc --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby28462.go @@ -0,0 +1,125 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/28462 + * Buggy version: b184bdabf7a01c4b802304ac64ac133743c484be + * fix commit-id: 89b123473774248fc3a0356dd3ce5b116cc69b29 + * Flaky: 69/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby28462", Moby28462) +} + +type State_moby28462 struct { + Health *Health_moby28462 +} + +type Container_moby28462 struct { + sync.Mutex + State *State_moby28462 +} + +func (ctr *Container_moby28462) start() { + go ctr.waitExit() +} +func (ctr *Container_moby28462) waitExit() { + +} + +type Store_moby28462 struct { + ctr *Container_moby28462 +} + +func (s *Store_moby28462) Get() *Container_moby28462 { + return s.ctr +} + +type Daemon_moby28462 struct { + containers Store_moby28462 +} + +func (d *Daemon_moby28462) StateChanged() { + c := d.containers.Get() + c.Lock() + d.updateHealthMonitorElseBranch(c) + defer c.Unlock() +} + +func (d *Daemon_moby28462) updateHealthMonitorIfBranch(c *Container_moby28462) { + h := c.State.Health + if stop := h.OpenMonitorChannel(); stop != nil { + go monitor_moby28462(c, stop) + } +} +func (d *Daemon_moby28462) updateHealthMonitorElseBranch(c *Container_moby28462) { + h := c.State.Health + h.CloseMonitorChannel() +} + +type Health_moby28462 struct { + stop chan struct{} +} + +func (s *Health_moby28462) OpenMonitorChannel() chan struct{} { + return s.stop +} + +func (s *Health_moby28462) CloseMonitorChannel() { + if s.stop != nil { + s.stop <- struct{}{} + } +} + +func monitor_moby28462(c *Container_moby28462, stop chan struct{}) { + for { + select { + case <-stop: + return + default: + handleProbeResult_moby28462(c) + } + } +} + +func handleProbeResult_moby28462(c *Container_moby28462) { + runtime.Gosched() + c.Lock() + defer c.Unlock() +} + +func NewDaemonAndContainer_moby28462() (*Daemon_moby28462, *Container_moby28462) { + c := &Container_moby28462{ + State: &State_moby28462{&Health_moby28462{make(chan struct{})}}, + } + d := &Daemon_moby28462{Store_moby28462{c}} + return d, c +} + +func Moby28462() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + d, c := NewDaemonAndContainer_moby28462() + go monitor_moby28462(c, c.State.Health.OpenMonitorChannel()) // G1 + go d.StateChanged() // G2 + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby30408.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby30408.go new file mode 100644 index 00000000000000..561e2faf576ab0 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby30408.go @@ -0,0 +1,67 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Moby30408", Moby30408) +} + +type Manifest_moby30408 struct { + Implements []string +} + +type Plugin_moby30408 struct { + activateWait *sync.Cond + activateErr error + Manifest *Manifest_moby30408 +} + +func (p *Plugin_moby30408) waitActive() error { + p.activateWait.L.Lock() + for !p.activated() { + p.activateWait.Wait() + } + p.activateWait.L.Unlock() + return p.activateErr +} + +func (p *Plugin_moby30408) activated() bool { + return p.Manifest != nil +} + +func testActive_moby30408(p *Plugin_moby30408) { + done := make(chan struct{}) + go func() { // G2 + p.waitActive() + close(done) + }() + <-done +} + +func Moby30408() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + go func() { // G1 + p := &Plugin_moby30408{activateWait: sync.NewCond(&sync.Mutex{})} + p.activateErr = errors.New("some junk happened") + + testActive_moby30408(p) + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby33781.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby33781.go new file mode 100644 index 00000000000000..4be50546e9be2c --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby33781.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/33781 + * Buggy version: 33fd3817b0f5ca4b87f0a75c2bd583b4425d392b + * fix commit-id: 67297ba0051d39be544009ba76abea14bc0be8a4 + * Flaky: 25/100 + */ + +package main + +import ( + "context" + "os" + "runtime/pprof" + "time" +) + +func init() { + register("Moby33781", Moby33781) +} + +func monitor_moby33781(stop chan bool) { + probeInterval := time.Millisecond + probeTimeout := time.Millisecond + for { + select { + case <-stop: + return + case <-time.After(probeInterval): + results := make(chan bool) + ctx, cancelProbe := context.WithTimeout(context.Background(), probeTimeout) + go func() { // G3 + results <- true + close(results) + }() + select { + case <-stop: + // results should be drained here + cancelProbe() + return + case <-results: + cancelProbe() + case <-ctx.Done(): + cancelProbe() + <-results + } + } + } +} + +func Moby33781() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + for i := 0; i < 100; i++ { + go func(i int) { + stop := make(chan bool) + go monitor_moby33781(stop) // G1 + go func() { // G2 + time.Sleep(time.Duration(i) * time.Millisecond) + stop <- true + }() + }(i) + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby36114.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby36114.go new file mode 100644 index 00000000000000..577c81651a0668 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby36114.go @@ -0,0 +1,53 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/36114 + * Buggy version: 6d4d3c52ae7c3f910bfc7552a2a673a8338e5b9f + * fix commit-id: a44fcd3d27c06aaa60d8d1cbce169f0d982e74b1 + * Flaky: 100/100 + */ +package main + +import ( + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby36114", Moby36114) +} + +type serviceVM_moby36114 struct { + sync.Mutex +} + +func (svm *serviceVM_moby36114) hotAddVHDsAtStart() { + svm.Lock() + defer svm.Unlock() + svm.hotRemoveVHDsAtStart() +} + +func (svm *serviceVM_moby36114) hotRemoveVHDsAtStart() { + svm.Lock() + defer svm.Unlock() +} + +func Moby36114() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 100; i++ { + go func() { + s := &serviceVM_moby36114{} + go s.hotAddVHDsAtStart() + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby4951.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby4951.go new file mode 100644 index 00000000000000..7f0497648cf606 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby4951.go @@ -0,0 +1,101 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/4951 + * Buggy version: 81f148be566ab2b17810ad4be61a5d8beac8330f + * fix commit-id: 2ffef1b7eb618162673c6ffabccb9ca57c7dfce3 + * Flaky: 100/100 + */ +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby4951", Moby4951) +} + +type DeviceSet_moby4951 struct { + sync.Mutex + infos map[string]*DevInfo_moby4951 + nrDeletedDevices int +} + +func (devices *DeviceSet_moby4951) DeleteDevice(hash string) { + devices.Lock() + defer devices.Unlock() + + info := devices.lookupDevice(hash) + + info.lock.Lock() + runtime.Gosched() + defer info.lock.Unlock() + + devices.deleteDevice(info) +} + +func (devices *DeviceSet_moby4951) lookupDevice(hash string) *DevInfo_moby4951 { + existing, ok := devices.infos[hash] + if !ok { + return nil + } + return existing +} + +func (devices *DeviceSet_moby4951) deleteDevice(info *DevInfo_moby4951) { + devices.removeDeviceAndWait(info.Name()) +} + +func (devices *DeviceSet_moby4951) removeDeviceAndWait(devname string) { + /// remove devices by devname + devices.Unlock() + time.Sleep(300 * time.Nanosecond) + devices.Lock() +} + +type DevInfo_moby4951 struct { + lock sync.Mutex + name string +} + +func (info *DevInfo_moby4951) Name() string { + return info.name +} + +func NewDeviceSet_moby4951() *DeviceSet_moby4951 { + devices := &DeviceSet_moby4951{ + infos: make(map[string]*DevInfo_moby4951), + } + info1 := &DevInfo_moby4951{ + name: "info1", + } + info2 := &DevInfo_moby4951{ + name: "info2", + } + devices.infos[info1.name] = info1 + devices.infos[info2.name] = info2 + return devices +} + +func Moby4951() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + go func() { + ds := NewDeviceSet_moby4951() + /// Delete devices by the same info + go ds.DeleteDevice("info1") + go ds.DeleteDevice("info1") + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/moby7559.go b/src/runtime/testdata/testgoroutineleakprofile/goker/moby7559.go new file mode 100644 index 00000000000000..212f65b1f3f2c6 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/moby7559.go @@ -0,0 +1,55 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +/* + * Project: moby + * Issue or PR : https://github.com/moby/moby/pull/7559 + * Buggy version: 64579f51fcb439c36377c0068ccc9a007b368b5a + * fix commit-id: 6cbb8e070d6c3a66bf48fbe5cbf689557eee23db + * Flaky: 100/100 + */ +package main + +import ( + "net" + "os" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Moby7559", Moby7559) +} + +type UDPProxy_moby7559 struct { + connTrackLock sync.Mutex +} + +func (proxy *UDPProxy_moby7559) Run() { + for i := 0; i < 2; i++ { + proxy.connTrackLock.Lock() + _, err := net.DialUDP("udp", nil, nil) + if err != nil { + continue + /// Missing unlock here + } + if i == 0 { + break + } + } + proxy.connTrackLock.Unlock() +} + +func Moby7559() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 20; i++ { + go (&UDPProxy_moby7559{}).Run() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/serving2137.go b/src/runtime/testdata/testgoroutineleakprofile/goker/serving2137.go new file mode 100644 index 00000000000000..49905315a01b4c --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/serving2137.go @@ -0,0 +1,122 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +func init() { + register("Serving2137", Serving2137) +} + +type token_serving2137 struct{} + +type request_serving2137 struct { + lock *sync.Mutex + accepted chan bool +} + +type Breaker_serving2137 struct { + pendingRequests chan token_serving2137 + activeRequests chan token_serving2137 +} + +func (b *Breaker_serving2137) Maybe(thunk func()) bool { + var t token_serving2137 + select { + default: + // Pending request queue is full. Report failure. + return false + case b.pendingRequests <- t: + // Pending request has capacity. + // Wait for capacity in the active queue. + b.activeRequests <- t + // Defer releasing capacity in the active and pending request queue. + defer func() { + <-b.activeRequests + runtime.Gosched() + <-b.pendingRequests + }() + // Do the thing. + thunk() + // Report success + return true + } +} + +func (b *Breaker_serving2137) concurrentRequest() request_serving2137 { + r := request_serving2137{lock: &sync.Mutex{}, accepted: make(chan bool, 1)} + r.lock.Lock() + var start sync.WaitGroup + start.Add(1) + go func() { // G2, G3 + start.Done() + runtime.Gosched() + ok := b.Maybe(func() { + // Will block on locked mutex. + r.lock.Lock() + runtime.Gosched() + r.lock.Unlock() + }) + r.accepted <- ok + }() + start.Wait() // Ensure that the go func has had a chance to execute. + return r +} + +// Perform n requests against the breaker, returning mutexes for each +// request which succeeded, and a slice of bools for all requests. +func (b *Breaker_serving2137) concurrentRequests(n int) []request_serving2137 { + requests := make([]request_serving2137, n) + for i := range requests { + requests[i] = b.concurrentRequest() + } + return requests +} + +func NewBreaker_serving2137(queueDepth, maxConcurrency int32) *Breaker_serving2137 { + return &Breaker_serving2137{ + pendingRequests: make(chan token_serving2137, queueDepth+maxConcurrency), + activeRequests: make(chan token_serving2137, maxConcurrency), + } +} + +func unlock_serving2137(req request_serving2137) { + req.lock.Unlock() + runtime.Gosched() + // Verify that function has completed + ok := <-req.accepted + runtime.Gosched() + // Requeue for next usage + req.accepted <- ok +} + +func unlockAll_serving2137(requests []request_serving2137) { + for _, lc := range requests { + unlock_serving2137(lc) + } +} + +func Serving2137() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(100 * time.Millisecond) + prof.WriteTo(os.Stdout, 2) + }() + + for i := 0; i < 1000; i++ { + go func() { + b := NewBreaker_serving2137(1, 1) + + locks := b.concurrentRequests(2) // G1 + unlockAll_serving2137(locks) + }() + } +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/syncthing4829.go b/src/runtime/testdata/testgoroutineleakprofile/goker/syncthing4829.go new file mode 100644 index 00000000000000..7967db7cfe083f --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/syncthing4829.go @@ -0,0 +1,87 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Syncthing4829", Syncthing4829) +} + +type Address_syncthing4829 int + +type Mapping_syncthing4829 struct { + mut sync.RWMutex // L2 + + extAddresses map[string]Address_syncthing4829 +} + +func (m *Mapping_syncthing4829) clearAddresses() { + m.mut.Lock() // L2 + var removed []Address_syncthing4829 + for id, addr := range m.extAddresses { + removed = append(removed, addr) + delete(m.extAddresses, id) + } + if len(removed) > 0 { + m.notify(nil, removed) + } + m.mut.Unlock() // L2 +} + +func (m *Mapping_syncthing4829) notify(added, remove []Address_syncthing4829) { + m.mut.RLock() // L2 + m.mut.RUnlock() // L2 +} + +type Service_syncthing4829 struct { + mut sync.RWMutex // L1 + + mappings []*Mapping_syncthing4829 +} + +func (s *Service_syncthing4829) NewMapping() *Mapping_syncthing4829 { + mapping := &Mapping_syncthing4829{ + extAddresses: make(map[string]Address_syncthing4829), + } + s.mut.Lock() // L1 + s.mappings = append(s.mappings, mapping) + s.mut.Unlock() // L1 + return mapping +} + +func (s *Service_syncthing4829) RemoveMapping(mapping *Mapping_syncthing4829) { + s.mut.Lock() // L1 + defer s.mut.Unlock() // L1 + for _, existing := range s.mappings { + if existing == mapping { + mapping.clearAddresses() + } + } +} + +func Syncthing4829() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + + go func() { // G1 + natSvc := &Service_syncthing4829{} + m := natSvc.NewMapping() + m.extAddresses["test"] = 0 + + natSvc.RemoveMapping(m) + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/goker/syncthing5795.go b/src/runtime/testdata/testgoroutineleakprofile/goker/syncthing5795.go new file mode 100644 index 00000000000000..e25494a688dd5c --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/goker/syncthing5795.go @@ -0,0 +1,103 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +func init() { + register("Syncthing5795", Syncthing5795) +} + +type message_syncthing5795 interface{} + +type ClusterConfig_syncthing5795 struct{} + +type Model_syncthing5795 interface { + ClusterConfig(message_syncthing5795) +} + +type TestModel_syncthing5795 struct { + ccFn func() +} + +func (t *TestModel_syncthing5795) ClusterConfig(msg message_syncthing5795) { + if t.ccFn != nil { + t.ccFn() + } +} + +type Connection_syncthing5795 interface { + Start() + Close() +} + +type rawConnection_syncthing5795 struct { + receiver Model_syncthing5795 + + inbox chan message_syncthing5795 + dispatcherLoopStopped chan struct{} + closed chan struct{} + closeOnce sync.Once +} + +func (c *rawConnection_syncthing5795) Start() { + go c.dispatcherLoop() // G2 +} + +func (c *rawConnection_syncthing5795) dispatcherLoop() { + defer close(c.dispatcherLoopStopped) + var msg message_syncthing5795 + for { + select { + case msg = <-c.inbox: + case <-c.closed: + return + } + switch msg := msg.(type) { + case *ClusterConfig_syncthing5795: + c.receiver.ClusterConfig(msg) + default: + return + } + } +} + +func (c *rawConnection_syncthing5795) Close() { + c.closeOnce.Do(func() { + close(c.closed) + <-c.dispatcherLoopStopped + }) +} + +func Syncthing5795() { + prof := pprof.Lookup("goroutineleak") + defer func() { + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) + }() + go func() { // G1 + m := &TestModel_syncthing5795{} + c := &rawConnection_syncthing5795{ + dispatcherLoopStopped: make(chan struct{}), + closed: make(chan struct{}), + inbox: make(chan message_syncthing5795), + receiver: m, + } + m.ccFn = c.Close + + c.Start() + c.inbox <- &ClusterConfig_syncthing5795{} + + <-c.dispatcherLoopStopped + }() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/main.go b/src/runtime/testdata/testgoroutineleakprofile/main.go new file mode 100644 index 00000000000000..5787c1e2b2bb90 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/main.go @@ -0,0 +1,39 @@ +// 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 + +import "os" + +// The number of times the main (profiling) goroutine should yield +// in order to allow the leaking goroutines to get stuck. +const yieldCount = 10 + +var cmds = map[string]func(){} + +func register(name string, f func()) { + if cmds[name] != nil { + panic("duplicate registration: " + name) + } + cmds[name] = f +} + +func registerInit(name string, f func()) { + if len(os.Args) >= 2 && os.Args[1] == name { + f() + } +} + +func main() { + if len(os.Args) < 2 { + println("usage: " + os.Args[0] + " name-of-test") + return + } + f := cmds[os.Args[1]] + if f == nil { + println("unknown function: " + os.Args[1]) + return + } + f() +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/simple.go b/src/runtime/testdata/testgoroutineleakprofile/simple.go new file mode 100644 index 00000000000000..b8172cd6df8aa7 --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/simple.go @@ -0,0 +1,253 @@ +// 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 + +import ( + "os" + "runtime" + "runtime/pprof" + "sync" +) + +// This is a set of micro-tests with obvious goroutine leaks that +// ensures goroutine leak detection works. +// +// Tests in this file are not flaky iff. run with GOMAXPROCS=1. +// The main goroutine forcefully yields via `runtime.Gosched()` before +// running the profiler. This moves them to the back of the run queue, +// allowing the leaky goroutines to be scheduled beforehand and get stuck. + +func init() { + register("NilRecv", NilRecv) + register("NilSend", NilSend) + register("SelectNoCases", SelectNoCases) + register("ChanRecv", ChanRecv) + register("ChanSend", ChanSend) + register("Select", Select) + register("WaitGroup", WaitGroup) + register("MutexStack", MutexStack) + register("MutexHeap", MutexHeap) + register("RWMutexRLock", RWMutexRLock) + register("RWMutexLock", RWMutexLock) + register("Cond", Cond) + register("Mixed", Mixed) + register("NoLeakGlobal", NoLeakGlobal) +} + +func NilRecv() { + prof := pprof.Lookup("goroutineleak") + go func() { + var c chan int + <-c + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func NilSend() { + prof := pprof.Lookup("goroutineleak") + go func() { + var c chan int + c <- 0 + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func ChanRecv() { + prof := pprof.Lookup("goroutineleak") + go func() { + <-make(chan int) + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func SelectNoCases() { + prof := pprof.Lookup("goroutineleak") + go func() { + select {} + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func ChanSend() { + prof := pprof.Lookup("goroutineleak") + go func() { + make(chan int) <- 0 + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func Select() { + prof := pprof.Lookup("goroutineleak") + go func() { + select { + case make(chan int) <- 0: + case <-make(chan int): + } + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func WaitGroup() { + prof := pprof.Lookup("goroutineleak") + go func() { + var wg sync.WaitGroup + wg.Add(1) + wg.Wait() + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func MutexStack() { + prof := pprof.Lookup("goroutineleak") + for i := 0; i < 1000; i++ { + go func() { + var mu sync.Mutex + mu.Lock() + mu.Lock() + panic("should not be reached") + }() + } + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func MutexHeap() { + prof := pprof.Lookup("goroutineleak") + for i := 0; i < 1000; i++ { + go func() { + mu := &sync.Mutex{} + go func() { + mu.Lock() + mu.Lock() + panic("should not be reached") + }() + }() + } + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func RWMutexRLock() { + prof := pprof.Lookup("goroutineleak") + go func() { + mu := &sync.RWMutex{} + mu.Lock() + mu.RLock() + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func RWMutexLock() { + prof := pprof.Lookup("goroutineleak") + go func() { + mu := &sync.RWMutex{} + mu.Lock() + mu.Lock() + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func Cond() { + prof := pprof.Lookup("goroutineleak") + go func() { + cond := sync.NewCond(&sync.Mutex{}) + cond.L.Lock() + cond.Wait() + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +func Mixed() { + prof := pprof.Lookup("goroutineleak") + go func() { + ch := make(chan int) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + ch <- 0 + wg.Done() + panic("should not be reached") + }() + wg.Wait() + <-ch + panic("should not be reached") + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} + +var ch = make(chan int) + +// No leak should be reported by this test +func NoLeakGlobal() { + prof := pprof.Lookup("goroutineleak") + go func() { + <-ch + }() + // Yield several times to allow the child goroutine to run. + for i := 0; i < yieldCount; i++ { + runtime.Gosched() + } + prof.WriteTo(os.Stdout, 2) +} diff --git a/src/runtime/testdata/testgoroutineleakprofile/stresstests.go b/src/runtime/testdata/testgoroutineleakprofile/stresstests.go new file mode 100644 index 00000000000000..64b535f51c87ef --- /dev/null +++ b/src/runtime/testdata/testgoroutineleakprofile/stresstests.go @@ -0,0 +1,89 @@ +// 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 + +import ( + "io" + "os" + "runtime" + "runtime/pprof" + "sync" + "time" +) + +const spawnGCMaxDepth = 5 + +func init() { + register("SpawnGC", SpawnGC) + register("DaisyChain", DaisyChain) +} + +func spawnGC(i int) { + prof := pprof.Lookup("goroutineleak") + if i == 0 { + return + } + wg := &sync.WaitGroup{} + wg.Add(i + 1) + go func() { + wg.Done() + <-make(chan int) + }() + for j := 0; j < i; j++ { + go func() { + wg.Done() + spawnGC(i - 1) + }() + } + wg.Wait() + runtime.Gosched() + if i == spawnGCMaxDepth { + prof.WriteTo(os.Stdout, 2) + } else { + // We want to concurrently trigger the profile in order to concurrently run + // the GC, but we don't want to stream all the profiles to standard output. + // + // Only output the profile for the root call to spawnGC, and otherwise stream + // the profile outputs to /dev/null to avoid jumbling. + prof.WriteTo(io.Discard, 2) + } +} + +// SpawnGC spawns a tree of goroutine leaks and calls the goroutine leak profiler +// for each node in the tree. It is supposed to stress the goroutine leak profiler +// under a heavily concurrent workload. +func SpawnGC() { + spawnGC(spawnGCMaxDepth) +} + +// DaisyChain spawns a daisy-chain of runnable goroutines. +// +// Each goroutine in the chain creates a new channel and goroutine. +// +// This illustrates a pathological worstcase for the goroutine leak GC complexity, +// as opposed to the regular GC, which is not negatively affected by this pattern. +func DaisyChain() { + prof := pprof.Lookup("goroutineleak") + defer func() { + time.Sleep(time.Second) + prof.WriteTo(os.Stdout, 2) + }() + var chain func(i int, ch chan struct{}) + chain = func(i int, ch chan struct{}) { + if i <= 0 { + go func() { + time.Sleep(time.Hour) + ch <- struct{}{} + }() + return + } + ch2 := make(chan struct{}) + go chain(i-1, ch2) + <-ch2 + ch <- struct{}{} + } + // The channel buffer avoids goroutine leaks. + go chain(1000, make(chan struct{}, 1)) +} diff --git a/src/runtime/traceback.go b/src/runtime/traceback.go index 53f41bca0b11ff..8882c306edb736 100644 --- a/src/runtime/traceback.go +++ b/src/runtime/traceback.go @@ -1206,6 +1206,7 @@ var gStatusStrings = [...]string{ _Gwaiting: "waiting", _Gdead: "dead", _Gcopystack: "copystack", + _Gleaked: "leaked", _Gpreempted: "preempted", } @@ -1226,7 +1227,7 @@ func goroutineheader(gp *g) { } // Override. - if gpstatus == _Gwaiting && gp.waitreason != waitReasonZero { + if (gpstatus == _Gwaiting || gpstatus == _Gleaked) && gp.waitreason != waitReasonZero { status = gp.waitreason.String() } @@ -1245,6 +1246,9 @@ func goroutineheader(gp *g) { } } print(" [", status) + if gpstatus == _Gleaked { + print(" (leaked)") + } if isScan { print(" (scan)") } diff --git a/src/runtime/tracestatus.go b/src/runtime/tracestatus.go index 03ec81fc0262a1..8b5eafd170f488 100644 --- a/src/runtime/tracestatus.go +++ b/src/runtime/tracestatus.go @@ -122,7 +122,7 @@ func goStatusToTraceGoStatus(status uint32, wr waitReason) tracev2.GoStatus { tgs = tracev2.GoRunning case _Gsyscall: tgs = tracev2.GoSyscall - case _Gwaiting, _Gpreempted: + case _Gwaiting, _Gpreempted, _Gleaked: // There are a number of cases where a G might end up in // _Gwaiting but it's actually running in a non-preemptive // state but needs to present itself as preempted to the From 707454b41fa1fea7e0286b1370dea47d3422b2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sat, 30 Aug 2025 21:13:12 +0100 Subject: [PATCH 35/63] cmd/go: update `go help mod edit` with the tool and ignore sections The types were added to the docs, but not the fields in GoMod. While here, I was initially confused about what is the top-level type given that `type Module` comes first. Move `type GoMod` to the top as it is the actual top-level type. Change-Id: I1270154837501f5c7f5b21959b2841fd4ac808d0 Reviewed-on: https://go-review.googlesource.com/c/go/+/700116 Reviewed-by: Sean Liao Reviewed-by: Carlos Amedee LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob Reviewed-by: Junyang Shao --- src/cmd/go/alldocs.go | 12 +++++++----- src/cmd/go/internal/modcmd/edit.go | 12 +++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index f1e1b1c333542c..19b48f0579bb29 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -1280,11 +1280,6 @@ // The -json flag prints the final go.mod file in JSON format instead of // writing it back to go.mod. The JSON output corresponds to these Go types: // -// type Module struct { -// Path string -// Version string -// } -// // type GoMod struct { // Module ModPath // Go string @@ -1294,6 +1289,13 @@ // Exclude []Module // Replace []Replace // Retract []Retract +// Tool []Tool +// Ignore []Ignore +// } +// +// type Module struct { +// Path string +// Version string // } // // type ModPath struct { diff --git a/src/cmd/go/internal/modcmd/edit.go b/src/cmd/go/internal/modcmd/edit.go index aafd9752a8e487..041b4432bfd4f0 100644 --- a/src/cmd/go/internal/modcmd/edit.go +++ b/src/cmd/go/internal/modcmd/edit.go @@ -104,11 +104,6 @@ writing it back to go.mod. The -json flag prints the final go.mod file in JSON format instead of writing it back to go.mod. The JSON output corresponds to these Go types: - type Module struct { - Path string - Version string - } - type GoMod struct { Module ModPath Go string @@ -118,6 +113,13 @@ writing it back to go.mod. The JSON output corresponds to these Go types: Exclude []Module Replace []Replace Retract []Retract + Tool []Tool + Ignore []Ignore + } + + type Module struct { + Path string + Version string } type ModPath struct { From f03c392295cfd57c29c92fcc300181f8016cf5ac Mon Sep 17 00:00:00 2001 From: qmuntal Date: Wed, 1 Oct 2025 10:59:53 +0200 Subject: [PATCH 36/63] runtime: fix aix/ppc64 library initialization AIX sets the argc and argv parameters in R14 and R15, but _rt0_ppc64x_lib expects them to be in R3 and R4. Also, call reginit in _rt0_ppc64x_lib. These issues were oversights from CL 706395 which went unnoticed because there if no LUCI aix/ppc64 builder (see #67299). Change-Id: I93a2798739935fbcead3e6162b4b90db7e740aa5 Reviewed-on: https://go-review.googlesource.com/c/go/+/708255 Reviewed-by: Carlos Amedee LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao Reviewed-by: Paul Murphy --- src/runtime/asm_ppc64x.s | 3 +++ src/runtime/rt0_aix_ppc64.s | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/runtime/asm_ppc64x.s b/src/runtime/asm_ppc64x.s index 3fbf11b5e9a2ad..b42e0b62f850ff 100644 --- a/src/runtime/asm_ppc64x.s +++ b/src/runtime/asm_ppc64x.s @@ -21,6 +21,9 @@ TEXT _rt0_ppc64x_lib(SB),NOSPLIT|NOFRAME,$0 MOVD R4, _rt0_ppc64x_lib_argv<>(SB) // Synchronous initialization. + MOVD $runtime·reginit(SB), R12 + MOVD R12, CTR + BL (CTR) MOVD $runtime·libpreinit(SB), R12 MOVD R12, CTR BL (CTR) diff --git a/src/runtime/rt0_aix_ppc64.s b/src/runtime/rt0_aix_ppc64.s index 32f8c72156c9ee..518fcb3b88d670 100644 --- a/src/runtime/rt0_aix_ppc64.s +++ b/src/runtime/rt0_aix_ppc64.s @@ -42,4 +42,6 @@ TEXT _main(SB),NOSPLIT,$-8 BR (CTR) TEXT _rt0_ppc64_aix_lib(SB),NOSPLIT,$0 + MOVD R14, R3 // argc + MOVD R15, R4 // argv JMP _rt0_ppc64x_lib(SB) From 0e4e2e68323df08d9e4c876e5abc5b549bd247f5 Mon Sep 17 00:00:00 2001 From: Michael Anthony Knyszek Date: Thu, 2 Oct 2025 19:15:34 +0000 Subject: [PATCH 37/63] runtime: skip TestGoroutineLeakProfile under mayMoreStackPreempt This may be the long-term fix, but we first need to understand if this just makes the tests flaky, or if it's revealing an actual underlying issue. I'm leaning toward the former. If it is the former, ideally we just make the tests robust (wait longer, maybe?). For now, this change will make the longtest builders OK again. For #75729. Change-Id: If9b30107d04a8e5af5670850add3a53f9471eec6 Reviewed-on: https://go-review.googlesource.com/c/go/+/708715 Reviewed-by: Roland Shoemaker Reviewed-by: Michael Pratt Auto-Submit: Michael Knyszek LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- src/runtime/goroutineleakprofile_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/runtime/goroutineleakprofile_test.go b/src/runtime/goroutineleakprofile_test.go index 9857d725deef32..6e26bcab132831 100644 --- a/src/runtime/goroutineleakprofile_test.go +++ b/src/runtime/goroutineleakprofile_test.go @@ -6,12 +6,20 @@ package runtime_test import ( "fmt" + "internal/testenv" + "os" "regexp" "strings" "testing" ) func TestGoroutineLeakProfile(t *testing.T) { + if strings.Contains(os.Getenv("GOFLAGS"), "mayMoreStackPreempt") { + // Some tests have false negatives under mayMoreStackPreempt. This may be a test-only issue, + // but needs more investigation. + testenv.SkipFlaky(t, 75729) + } + // Goroutine leak test case. // // Test cases can be configured with test name, the name of the entry point function, @@ -356,7 +364,7 @@ func TestGoroutineLeakProfile(t *testing.T) { `\(\*Page_hugo5379\)\.initContent\.func1\.1\(.* \[sync\.Mutex\.Lock\]`, `pageRenderer_hugo5379\(.* \[sync\.Mutex\.Lock\]`, `Hugo5379\.func2\(.* \[sync\.WaitGroup\.Wait\]`, - ), + ), makeFlakyTest("Istio16224", `Istio16224\.func2\(.* \[sync\.Mutex\.Lock\]`, `\(\*controller_istio16224\)\.Run\(.* \[chan send\]`, From 4008e07080ef215e46f48e5e2f6b5d37d6d9cb9f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 2 Oct 2025 10:45:01 -0700 Subject: [PATCH 38/63] io/fs: move path name documentation up to the package doc comment Perhaps surprisingly to users, io/fs path names are slash-separated. Move the documentation for path names up to the top of the package rather than burying it in the ValidPath documentation. Change-Id: Id338df07c74a16be74c687ac4c45e0513ee40a8c Reviewed-on: https://go-review.googlesource.com/c/go/+/708616 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Damien Neil --- src/io/fs/fs.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/io/fs/fs.go b/src/io/fs/fs.go index 8f693f2574a5b6..fca07b818c9a3b 100644 --- a/src/io/fs/fs.go +++ b/src/io/fs/fs.go @@ -6,6 +6,19 @@ // A file system can be provided by the host operating system // but also by other packages. // +// # Path Names +// +// The interfaces in this package all operate on the same +// path name syntax, regardless of the host operating system. +// +// Path names are UTF-8-encoded, +// unrooted, slash-separated sequences of path elements, like “x/y/z”. +// Path names must not contain an element that is “.” or “..” or the empty string, +// except for the special case that the name "." may be used for the root directory. +// Paths must not start or end with a slash: “/x” and “x/” are invalid. +// +// # Testing +// // See the [testing/fstest] package for support with testing // implementations of file systems. package fs @@ -41,16 +54,13 @@ type FS interface { // ValidPath reports whether the given path name // is valid for use in a call to Open. // -// Path names passed to open are UTF-8-encoded, -// unrooted, slash-separated sequences of path elements, like “x/y/z”. -// Path names must not contain an element that is “.” or “..” or the empty string, -// except for the special case that the name "." may be used for the root directory. -// Paths must not start or end with a slash: “/x” and “x/” are invalid. -// // Note that paths are slash-separated on all systems, even Windows. // Paths containing other characters such as backslash and colon // are accepted as valid, but those characters must never be // interpreted by an [FS] implementation as path element separators. +// See the [Path Names] section for more details. +// +// [Path Names]: https://pkg.go.dev/io/fs#hdr-Path_Names func ValidPath(name string) bool { if !utf8.ValidString(name) { return false From bbdff9e8e1fca772a13acb0c4c7828cfe246d403 Mon Sep 17 00:00:00 2001 From: "Nicholas S. Husin" Date: Thu, 2 Oct 2025 12:41:25 -0400 Subject: [PATCH 39/63] net/http: update bundled x/net/http2 and delete obsolete http2inTests http2inTests is no longer needed after go.dev/cl/708135 and should be deleted. To prevent errors in future vendored dependency updates, h2_bundle.go is also updated together in this change. Change-Id: I7b8c3f6854203fab4ec639a2a268df0cd2b1dee7 Reviewed-on: https://go-review.googlesource.com/c/go/+/708595 Reviewed-by: Damien Neil Reviewed-by: Nicholas Husin LUCI-TryBot-Result: Go LUCI --- src/go.mod | 2 +- src/go.sum | 4 +- src/net/http/h2_bundle.go | 415 ++++++++++++++---- src/net/http/http2_test.go | 12 - .../http/internal/httpcommon/httpcommon.go | 4 +- src/net/http/socks_bundle.go | 2 +- .../golang.org/x/net/nettest/conntest.go | 6 +- src/vendor/modules.txt | 2 +- 8 files changed, 336 insertions(+), 111 deletions(-) delete mode 100644 src/net/http/http2_test.go diff --git a/src/go.mod b/src/go.mod index 4b400fe87189d1..f134f0c7b571da 100644 --- a/src/go.mod +++ b/src/go.mod @@ -4,7 +4,7 @@ go 1.26 require ( golang.org/x/crypto v0.42.0 - golang.org/x/net v0.44.0 + golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f ) require ( diff --git a/src/go.sum b/src/go.sum index c90970a9cbaaad..f24bea029a2239 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,7 +1,7 @@ golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f h1:vNklv+oJQSYNGsWXHoCPi2MHMcpj9/Q7aBhvvfnJvGg= +golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= diff --git a/src/net/http/h2_bundle.go b/src/net/http/h2_bundle.go index 3abd7476f04e73..f09e102efb7b1c 100644 --- a/src/net/http/h2_bundle.go +++ b/src/net/http/h2_bundle.go @@ -1047,6 +1047,7 @@ func http2shouldRetryDial(call *http2dialCall, req *Request) bool { // - If the resulting value is zero or out of range, use a default. type http2http2Config struct { MaxConcurrentStreams uint32 + StrictMaxConcurrentRequests bool MaxDecoderHeaderTableSize uint32 MaxEncoderHeaderTableSize uint32 MaxReadFrameSize uint32 @@ -1084,12 +1085,13 @@ func http2configFromServer(h1 *Server, h2 *http2Server) http2http2Config { // (the net/http Transport). func http2configFromTransport(h2 *http2Transport) http2http2Config { conf := http2http2Config{ - MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize, - MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize, - MaxReadFrameSize: h2.MaxReadFrameSize, - SendPingTimeout: h2.ReadIdleTimeout, - PingTimeout: h2.PingTimeout, - WriteByteTimeout: h2.WriteByteTimeout, + StrictMaxConcurrentRequests: h2.StrictMaxConcurrentStreams, + MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize, + MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize, + MaxReadFrameSize: h2.MaxReadFrameSize, + SendPingTimeout: h2.ReadIdleTimeout, + PingTimeout: h2.PingTimeout, + WriteByteTimeout: h2.WriteByteTimeout, } // Unlike most config fields, where out-of-range values revert to the default, @@ -1148,6 +1150,9 @@ func http2fillNetHTTPConfig(conf *http2http2Config, h2 *HTTP2Config) { if h2.MaxConcurrentStreams != 0 { conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams) } + if http2http2ConfigStrictMaxConcurrentRequests(h2) { + conf.StrictMaxConcurrentRequests = true + } if h2.MaxEncoderHeaderTableSize != 0 { conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize) } @@ -1183,6 +1188,10 @@ func http2fillNetHTTPConfig(conf *http2http2Config, h2 *HTTP2Config) { } } +func http2http2ConfigStrictMaxConcurrentRequests(h2 *HTTP2Config) bool { + return h2.StrictMaxConcurrentRequests +} + // Buffer chunks are allocated from a pool to reduce pressure on GC. // The maximum wasted space per dataBuffer is 2x the largest size class, // which happens when the dataBuffer has multiple chunks and there is @@ -1900,7 +1909,7 @@ func (fr *http2Framer) maxHeaderListSize() uint32 { func (f *http2Framer) startWrite(ftype http2FrameType, flags http2Flags, streamID uint32) { // Write the FrameHeader. f.wbuf = append(f.wbuf[:0], - 0, // 3 bytes of length, filled in in endWrite + 0, // 3 bytes of length, filled in endWrite 0, 0, byte(ftype), @@ -2708,6 +2717,15 @@ type http2PriorityFrame struct { http2PriorityParam } +var http2defaultRFC9218Priority = http2PriorityParam{ + incremental: 0, + urgency: 3, +} + +// Note that HTTP/2 has had two different prioritization schemes, and +// PriorityParam struct below is a superset of both schemes. The exported +// symbols are from RFC 7540 and the non-exported ones are from RFC 9218. + // PriorityParam are the stream prioritzation parameters. type http2PriorityParam struct { // StreamDep is a 31-bit stream identifier for the @@ -2723,6 +2741,20 @@ type http2PriorityParam struct { // the spec, "Add one to the value to obtain a weight between // 1 and 256." Weight uint8 + + // "The urgency (u) parameter value is Integer (see Section 3.3.1 of + // [STRUCTURED-FIELDS]), between 0 and 7 inclusive, in descending order of + // priority. The default is 3." + urgency uint8 + + // "The incremental (i) parameter value is Boolean (see Section 3.3.6 of + // [STRUCTURED-FIELDS]). It indicates if an HTTP response can be processed + // incrementally, i.e., provide some meaningful output as chunks of the + // response arrive." + // + // We use uint8 (i.e. 0 is false, 1 is true) instead of bool so we can + // avoid unnecessary type conversions and because either type takes 1 byte. + incremental uint8 } func (p http2PriorityParam) IsZero() bool { @@ -3423,7 +3455,6 @@ var ( http2VerboseLogs bool http2logFrameWrites bool http2logFrameReads bool - http2inTests bool // Enabling extended CONNECT by causes browsers to attempt to use // WebSockets-over-HTTP/2. This results in problems when the server's websocket @@ -4103,6 +4134,10 @@ type http2Server struct { type http2serverInternalState struct { mu sync.Mutex activeConns map[*http2serverConn]struct{} + + // Pool of error channels. This is per-Server rather than global + // because channels can't be reused across synctest bubbles. + errChanPool sync.Pool } func (s *http2serverInternalState) registerConn(sc *http2serverConn) { @@ -4134,6 +4169,27 @@ func (s *http2serverInternalState) startGracefulShutdown() { s.mu.Unlock() } +// Global error channel pool used for uninitialized Servers. +// We use a per-Server pool when possible to avoid using channels across synctest bubbles. +var http2errChanPool = sync.Pool{ + New: func() any { return make(chan error, 1) }, +} + +func (s *http2serverInternalState) getErrChan() chan error { + if s == nil { + return http2errChanPool.Get().(chan error) // Server used without calling ConfigureServer + } + return s.errChanPool.Get().(chan error) +} + +func (s *http2serverInternalState) putErrChan(ch chan error) { + if s == nil { + http2errChanPool.Put(ch) // Server used without calling ConfigureServer + return + } + s.errChanPool.Put(ch) +} + // ConfigureServer adds HTTP/2 support to a net/http Server. // // The configuration conf may be nil. @@ -4146,7 +4202,10 @@ func http2ConfigureServer(s *Server, conf *http2Server) error { if conf == nil { conf = new(http2Server) } - conf.state = &http2serverInternalState{activeConns: make(map[*http2serverConn]struct{})} + conf.state = &http2serverInternalState{ + activeConns: make(map[*http2serverConn]struct{}), + errChanPool: sync.Pool{New: func() any { return make(chan error, 1) }}, + } if h1, h2 := s, conf; h2.IdleTimeout == 0 { if h1.IdleTimeout != 0 { h2.IdleTimeout = h1.IdleTimeout @@ -5052,25 +5111,6 @@ func (sc *http2serverConn) readPreface() error { } } -var http2errChanPool = sync.Pool{ - New: func() interface{} { return make(chan error, 1) }, -} - -func http2getErrChan() chan error { - if http2inTests { - // Channels cannot be reused across synctest tests. - return make(chan error, 1) - } else { - return http2errChanPool.Get().(chan error) - } -} - -func http2putErrChan(ch chan error) { - if !http2inTests { - http2errChanPool.Put(ch) - } -} - var http2writeDataPool = sync.Pool{ New: func() interface{} { return new(http2writeData) }, } @@ -5078,7 +5118,7 @@ var http2writeDataPool = sync.Pool{ // writeDataFromHandler writes DATA response frames from a handler on // the given stream. func (sc *http2serverConn) writeDataFromHandler(stream *http2stream, data []byte, endStream bool) error { - ch := http2getErrChan() + ch := sc.srv.state.getErrChan() writeArg := http2writeDataPool.Get().(*http2writeData) *writeArg = http2writeData{stream.id, data, endStream} err := sc.writeFrameFromHandler(http2FrameWriteRequest{ @@ -5110,7 +5150,7 @@ func (sc *http2serverConn) writeDataFromHandler(stream *http2stream, data []byte return http2errStreamClosed } } - http2putErrChan(ch) + sc.srv.state.putErrChan(ch) if frameWriteDone { http2writeDataPool.Put(writeArg) } @@ -6364,7 +6404,7 @@ func (sc *http2serverConn) writeHeaders(st *http2stream, headerData *http2writeR // waiting for this frame to be written, so an http.Flush mid-handler // writes out the correct value of keys, before a handler later potentially // mutates it. - errc = http2getErrChan() + errc = sc.srv.state.getErrChan() } if err := sc.writeFrameFromHandler(http2FrameWriteRequest{ write: headerData, @@ -6376,7 +6416,7 @@ func (sc *http2serverConn) writeHeaders(st *http2stream, headerData *http2writeR if errc != nil { select { case err := <-errc: - http2putErrChan(errc) + sc.srv.state.putErrChan(errc) return err case <-sc.doneServing: return http2errClientDisconnected @@ -7057,7 +7097,7 @@ func (w *http2responseWriter) Push(target string, opts *PushOptions) error { method: opts.Method, url: u, header: http2cloneHeader(opts.Header), - done: http2getErrChan(), + done: sc.srv.state.getErrChan(), } select { @@ -7074,7 +7114,7 @@ func (w *http2responseWriter) Push(target string, opts *PushOptions) error { case <-st.cw: return http2errStreamClosed case err := <-msg.done: - http2putErrChan(msg.done) + sc.srv.state.putErrChan(msg.done) return err } } @@ -7577,6 +7617,7 @@ type http2ClientConn struct { readIdleTimeout time.Duration pingTimeout time.Duration extendedConnectAllowed bool + strictMaxConcurrentStreams bool // rstStreamPingsBlocked works around an unfortunate gRPC behavior. // gRPC strictly limits the number of PING frames that it will receive. @@ -8007,7 +8048,8 @@ func (t *http2Transport) newClientConn(c net.Conn, singleUse bool) (*http2Client initialWindowSize: 65535, // spec default initialStreamRecvWindowSize: conf.MaxUploadBufferPerStream, maxConcurrentStreams: http2initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings. - peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead. + strictMaxConcurrentStreams: conf.StrictMaxConcurrentRequests, + peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead. streams: make(map[uint32]*http2clientStream), singleUse: singleUse, seenSettingsChan: make(chan struct{}), @@ -8241,7 +8283,7 @@ func (cc *http2ClientConn) idleStateLocked() (st http2clientConnIdleState) { return } var maxConcurrentOkay bool - if cc.t.StrictMaxConcurrentStreams { + if cc.strictMaxConcurrentStreams { // We'll tell the caller we can take a new request to // prevent the caller from dialing a new TCP // connection, but then we'll block later before @@ -10884,6 +10926,8 @@ type http2OpenStreamOptions struct { // PusherID is zero if the stream was initiated by the client. Otherwise, // PusherID names the stream that pushed the newly opened stream. PusherID uint32 + // priority is used to set the priority of the newly opened stream. + priority http2PriorityParam } // FrameWriteRequest is a request to write a frame. @@ -11095,7 +11139,7 @@ func (p *http2writeQueuePool) get() *http2writeQueue { } // RFC 7540, Section 5.3.5: the default weight is 16. -const http2priorityDefaultWeight = 15 // 16 = 15 + 1 +const http2priorityDefaultWeightRFC7540 = 15 // 16 = 15 + 1 // PriorityWriteSchedulerConfig configures a priorityWriteScheduler. type http2PriorityWriteSchedulerConfig struct { @@ -11150,8 +11194,8 @@ func http2NewPriorityWriteScheduler(cfg *http2PriorityWriteSchedulerConfig) http } } - ws := &http2priorityWriteScheduler{ - nodes: make(map[uint32]*http2priorityNode), + ws := &http2priorityWriteSchedulerRFC7540{ + nodes: make(map[uint32]*http2priorityNodeRFC7540), maxClosedNodesInTree: cfg.MaxClosedNodesInTree, maxIdleNodesInTree: cfg.MaxIdleNodesInTree, enableWriteThrottle: cfg.ThrottleOutOfOrderWrites, @@ -11165,32 +11209,32 @@ func http2NewPriorityWriteScheduler(cfg *http2PriorityWriteSchedulerConfig) http return ws } -type http2priorityNodeState int +type http2priorityNodeStateRFC7540 int const ( - http2priorityNodeOpen http2priorityNodeState = iota - http2priorityNodeClosed - http2priorityNodeIdle + http2priorityNodeOpenRFC7540 http2priorityNodeStateRFC7540 = iota + http2priorityNodeClosedRFC7540 + http2priorityNodeIdleRFC7540 ) -// priorityNode is a node in an HTTP/2 priority tree. +// priorityNodeRFC7540 is a node in an HTTP/2 priority tree. // Each node is associated with a single stream ID. // See RFC 7540, Section 5.3. -type http2priorityNode struct { - q http2writeQueue // queue of pending frames to write - id uint32 // id of the stream, or 0 for the root of the tree - weight uint8 // the actual weight is weight+1, so the value is in [1,256] - state http2priorityNodeState // open | closed | idle - bytes int64 // number of bytes written by this node, or 0 if closed - subtreeBytes int64 // sum(node.bytes) of all nodes in this subtree +type http2priorityNodeRFC7540 struct { + q http2writeQueue // queue of pending frames to write + id uint32 // id of the stream, or 0 for the root of the tree + weight uint8 // the actual weight is weight+1, so the value is in [1,256] + state http2priorityNodeStateRFC7540 // open | closed | idle + bytes int64 // number of bytes written by this node, or 0 if closed + subtreeBytes int64 // sum(node.bytes) of all nodes in this subtree // These links form the priority tree. - parent *http2priorityNode - kids *http2priorityNode // start of the kids list - prev, next *http2priorityNode // doubly-linked list of siblings + parent *http2priorityNodeRFC7540 + kids *http2priorityNodeRFC7540 // start of the kids list + prev, next *http2priorityNodeRFC7540 // doubly-linked list of siblings } -func (n *http2priorityNode) setParent(parent *http2priorityNode) { +func (n *http2priorityNodeRFC7540) setParent(parent *http2priorityNodeRFC7540) { if n == parent { panic("setParent to self") } @@ -11225,7 +11269,7 @@ func (n *http2priorityNode) setParent(parent *http2priorityNode) { } } -func (n *http2priorityNode) addBytes(b int64) { +func (n *http2priorityNodeRFC7540) addBytes(b int64) { n.bytes += b for ; n != nil; n = n.parent { n.subtreeBytes += b @@ -11238,7 +11282,7 @@ func (n *http2priorityNode) addBytes(b int64) { // // f(n, openParent) takes two arguments: the node to visit, n, and a bool that is true // if any ancestor p of n is still open (ignoring the root node). -func (n *http2priorityNode) walkReadyInOrder(openParent bool, tmp *[]*http2priorityNode, f func(*http2priorityNode, bool) bool) bool { +func (n *http2priorityNodeRFC7540) walkReadyInOrder(openParent bool, tmp *[]*http2priorityNodeRFC7540, f func(*http2priorityNodeRFC7540, bool) bool) bool { if !n.q.empty() && f(n, openParent) { return true } @@ -11249,7 +11293,7 @@ func (n *http2priorityNode) walkReadyInOrder(openParent bool, tmp *[]*http2prior // Don't consider the root "open" when updating openParent since // we can't send data frames on the root stream (only control frames). if n.id != 0 { - openParent = openParent || (n.state == http2priorityNodeOpen) + openParent = openParent || (n.state == http2priorityNodeOpenRFC7540) } // Common case: only one kid or all kids have the same weight. @@ -11279,7 +11323,7 @@ func (n *http2priorityNode) walkReadyInOrder(openParent bool, tmp *[]*http2prior *tmp = append(*tmp, n.kids) n.kids.setParent(nil) } - sort.Sort(http2sortPriorityNodeSiblings(*tmp)) + sort.Sort(http2sortPriorityNodeSiblingsRFC7540(*tmp)) for i := len(*tmp) - 1; i >= 0; i-- { (*tmp)[i].setParent(n) // setParent inserts at the head of n.kids } @@ -11291,13 +11335,13 @@ func (n *http2priorityNode) walkReadyInOrder(openParent bool, tmp *[]*http2prior return false } -type http2sortPriorityNodeSiblings []*http2priorityNode +type http2sortPriorityNodeSiblingsRFC7540 []*http2priorityNodeRFC7540 -func (z http2sortPriorityNodeSiblings) Len() int { return len(z) } +func (z http2sortPriorityNodeSiblingsRFC7540) Len() int { return len(z) } -func (z http2sortPriorityNodeSiblings) Swap(i, k int) { z[i], z[k] = z[k], z[i] } +func (z http2sortPriorityNodeSiblingsRFC7540) Swap(i, k int) { z[i], z[k] = z[k], z[i] } -func (z http2sortPriorityNodeSiblings) Less(i, k int) bool { +func (z http2sortPriorityNodeSiblingsRFC7540) Less(i, k int) bool { // Prefer the subtree that has sent fewer bytes relative to its weight. // See sections 5.3.2 and 5.3.4. wi, bi := float64(z[i].weight+1), float64(z[i].subtreeBytes) @@ -11311,13 +11355,13 @@ func (z http2sortPriorityNodeSiblings) Less(i, k int) bool { return bi/bk <= wi/wk } -type http2priorityWriteScheduler struct { +type http2priorityWriteSchedulerRFC7540 struct { // root is the root of the priority tree, where root.id = 0. // The root queues control frames that are not associated with any stream. - root http2priorityNode + root http2priorityNodeRFC7540 // nodes maps stream ids to priority tree nodes. - nodes map[uint32]*http2priorityNode + nodes map[uint32]*http2priorityNodeRFC7540 // maxID is the maximum stream id in nodes. maxID uint32 @@ -11325,7 +11369,7 @@ type http2priorityWriteScheduler struct { // lists of nodes that have been closed or are idle, but are kept in // the tree for improved prioritization. When the lengths exceed either // maxClosedNodesInTree or maxIdleNodesInTree, old nodes are discarded. - closedNodes, idleNodes []*http2priorityNode + closedNodes, idleNodes []*http2priorityNodeRFC7540 // From the config. maxClosedNodesInTree int @@ -11334,19 +11378,19 @@ type http2priorityWriteScheduler struct { enableWriteThrottle bool // tmp is scratch space for priorityNode.walkReadyInOrder to reduce allocations. - tmp []*http2priorityNode + tmp []*http2priorityNodeRFC7540 // pool of empty queues for reuse. queuePool http2writeQueuePool } -func (ws *http2priorityWriteScheduler) OpenStream(streamID uint32, options http2OpenStreamOptions) { +func (ws *http2priorityWriteSchedulerRFC7540) OpenStream(streamID uint32, options http2OpenStreamOptions) { // The stream may be currently idle but cannot be opened or closed. if curr := ws.nodes[streamID]; curr != nil { - if curr.state != http2priorityNodeIdle { + if curr.state != http2priorityNodeIdleRFC7540 { panic(fmt.Sprintf("stream %d already opened", streamID)) } - curr.state = http2priorityNodeOpen + curr.state = http2priorityNodeOpenRFC7540 return } @@ -11358,11 +11402,11 @@ func (ws *http2priorityWriteScheduler) OpenStream(streamID uint32, options http2 if parent == nil { parent = &ws.root } - n := &http2priorityNode{ + n := &http2priorityNodeRFC7540{ q: *ws.queuePool.get(), id: streamID, - weight: http2priorityDefaultWeight, - state: http2priorityNodeOpen, + weight: http2priorityDefaultWeightRFC7540, + state: http2priorityNodeOpenRFC7540, } n.setParent(parent) ws.nodes[streamID] = n @@ -11371,19 +11415,19 @@ func (ws *http2priorityWriteScheduler) OpenStream(streamID uint32, options http2 } } -func (ws *http2priorityWriteScheduler) CloseStream(streamID uint32) { +func (ws *http2priorityWriteSchedulerRFC7540) CloseStream(streamID uint32) { if streamID == 0 { panic("violation of WriteScheduler interface: cannot close stream 0") } if ws.nodes[streamID] == nil { panic(fmt.Sprintf("violation of WriteScheduler interface: unknown stream %d", streamID)) } - if ws.nodes[streamID].state != http2priorityNodeOpen { + if ws.nodes[streamID].state != http2priorityNodeOpenRFC7540 { panic(fmt.Sprintf("violation of WriteScheduler interface: stream %d already closed", streamID)) } n := ws.nodes[streamID] - n.state = http2priorityNodeClosed + n.state = http2priorityNodeClosedRFC7540 n.addBytes(-n.bytes) q := n.q @@ -11396,7 +11440,7 @@ func (ws *http2priorityWriteScheduler) CloseStream(streamID uint32) { } } -func (ws *http2priorityWriteScheduler) AdjustStream(streamID uint32, priority http2PriorityParam) { +func (ws *http2priorityWriteSchedulerRFC7540) AdjustStream(streamID uint32, priority http2PriorityParam) { if streamID == 0 { panic("adjustPriority on root") } @@ -11410,11 +11454,11 @@ func (ws *http2priorityWriteScheduler) AdjustStream(streamID uint32, priority ht return } ws.maxID = streamID - n = &http2priorityNode{ + n = &http2priorityNodeRFC7540{ q: *ws.queuePool.get(), id: streamID, - weight: http2priorityDefaultWeight, - state: http2priorityNodeIdle, + weight: http2priorityDefaultWeightRFC7540, + state: http2priorityNodeIdleRFC7540, } n.setParent(&ws.root) ws.nodes[streamID] = n @@ -11426,7 +11470,7 @@ func (ws *http2priorityWriteScheduler) AdjustStream(streamID uint32, priority ht parent := ws.nodes[priority.StreamDep] if parent == nil { n.setParent(&ws.root) - n.weight = http2priorityDefaultWeight + n.weight = http2priorityDefaultWeightRFC7540 return } @@ -11467,8 +11511,8 @@ func (ws *http2priorityWriteScheduler) AdjustStream(streamID uint32, priority ht n.weight = priority.Weight } -func (ws *http2priorityWriteScheduler) Push(wr http2FrameWriteRequest) { - var n *http2priorityNode +func (ws *http2priorityWriteSchedulerRFC7540) Push(wr http2FrameWriteRequest) { + var n *http2priorityNodeRFC7540 if wr.isControl() { n = &ws.root } else { @@ -11487,8 +11531,8 @@ func (ws *http2priorityWriteScheduler) Push(wr http2FrameWriteRequest) { n.q.push(wr) } -func (ws *http2priorityWriteScheduler) Pop() (wr http2FrameWriteRequest, ok bool) { - ws.root.walkReadyInOrder(false, &ws.tmp, func(n *http2priorityNode, openParent bool) bool { +func (ws *http2priorityWriteSchedulerRFC7540) Pop() (wr http2FrameWriteRequest, ok bool) { + ws.root.walkReadyInOrder(false, &ws.tmp, func(n *http2priorityNodeRFC7540, openParent bool) bool { limit := int32(math.MaxInt32) if openParent { limit = ws.writeThrottleLimit @@ -11514,7 +11558,7 @@ func (ws *http2priorityWriteScheduler) Pop() (wr http2FrameWriteRequest, ok bool return wr, ok } -func (ws *http2priorityWriteScheduler) addClosedOrIdleNode(list *[]*http2priorityNode, maxSize int, n *http2priorityNode) { +func (ws *http2priorityWriteSchedulerRFC7540) addClosedOrIdleNode(list *[]*http2priorityNodeRFC7540, maxSize int, n *http2priorityNodeRFC7540) { if maxSize == 0 { return } @@ -11528,7 +11572,7 @@ func (ws *http2priorityWriteScheduler) addClosedOrIdleNode(list *[]*http2priorit *list = append(*list, n) } -func (ws *http2priorityWriteScheduler) removeNode(n *http2priorityNode) { +func (ws *http2priorityWriteSchedulerRFC7540) removeNode(n *http2priorityNodeRFC7540) { for n.kids != nil { n.kids.setParent(n.parent) } @@ -11536,6 +11580,199 @@ func (ws *http2priorityWriteScheduler) removeNode(n *http2priorityNode) { delete(ws.nodes, n.id) } +type http2streamMetadata struct { + location *http2writeQueue + priority http2PriorityParam +} + +type http2priorityWriteSchedulerRFC9218 struct { + // control contains control frames (SETTINGS, PING, etc.). + control http2writeQueue + + // heads contain the head of a circular list of streams. + // We put these heads within a nested array that represents urgency and + // incremental, as defined in + // https://www.rfc-editor.org/rfc/rfc9218.html#name-priority-parameters. + // 8 represents u=0 up to u=7, and 2 represents i=false and i=true. + heads [8][2]*http2writeQueue + + // streams contains a mapping between each stream ID and their metadata, so + // we can quickly locate them when needing to, for example, adjust their + // priority. + streams map[uint32]http2streamMetadata + + // queuePool are empty queues for reuse. + queuePool http2writeQueuePool + + // prioritizeIncremental is used to determine whether we should prioritize + // incremental streams or not, when urgency is the same in a given Pop() + // call. + prioritizeIncremental bool +} + +func http2newPriorityWriteSchedulerRFC9128() http2WriteScheduler { + ws := &http2priorityWriteSchedulerRFC9218{ + streams: make(map[uint32]http2streamMetadata), + } + return ws +} + +func (ws *http2priorityWriteSchedulerRFC9218) OpenStream(streamID uint32, opt http2OpenStreamOptions) { + if ws.streams[streamID].location != nil { + panic(fmt.Errorf("stream %d already opened", streamID)) + } + q := ws.queuePool.get() + ws.streams[streamID] = http2streamMetadata{ + location: q, + priority: opt.priority, + } + + u, i := opt.priority.urgency, opt.priority.incremental + if ws.heads[u][i] == nil { + ws.heads[u][i] = q + q.next = q + q.prev = q + } else { + // Queues are stored in a ring. + // Insert the new stream before ws.head, putting it at the end of the list. + q.prev = ws.heads[u][i].prev + q.next = ws.heads[u][i] + q.prev.next = q + q.next.prev = q + } +} + +func (ws *http2priorityWriteSchedulerRFC9218) CloseStream(streamID uint32) { + metadata := ws.streams[streamID] + q, u, i := metadata.location, metadata.priority.urgency, metadata.priority.incremental + if q == nil { + return + } + if q.next == q { + // This was the only open stream. + ws.heads[u][i] = nil + } else { + q.prev.next = q.next + q.next.prev = q.prev + if ws.heads[u][i] == q { + ws.heads[u][i] = q.next + } + } + delete(ws.streams, streamID) + ws.queuePool.put(q) +} + +func (ws *http2priorityWriteSchedulerRFC9218) AdjustStream(streamID uint32, priority http2PriorityParam) { + metadata := ws.streams[streamID] + q, u, i := metadata.location, metadata.priority.urgency, metadata.priority.incremental + if q == nil { + return + } + + // Remove stream from current location. + if q.next == q { + // This was the only open stream. + ws.heads[u][i] = nil + } else { + q.prev.next = q.next + q.next.prev = q.prev + if ws.heads[u][i] == q { + ws.heads[u][i] = q.next + } + } + + // Insert stream to the new queue. + u, i = priority.urgency, priority.incremental + if ws.heads[u][i] == nil { + ws.heads[u][i] = q + q.next = q + q.prev = q + } else { + // Queues are stored in a ring. + // Insert the new stream before ws.head, putting it at the end of the list. + q.prev = ws.heads[u][i].prev + q.next = ws.heads[u][i] + q.prev.next = q + q.next.prev = q + } +} + +func (ws *http2priorityWriteSchedulerRFC9218) Push(wr http2FrameWriteRequest) { + if wr.isControl() { + ws.control.push(wr) + return + } + q := ws.streams[wr.StreamID()].location + if q == nil { + // This is a closed stream. + // wr should not be a HEADERS or DATA frame. + // We push the request onto the control queue. + if wr.DataSize() > 0 { + panic("add DATA on non-open stream") + } + ws.control.push(wr) + return + } + q.push(wr) +} + +func (ws *http2priorityWriteSchedulerRFC9218) Pop() (http2FrameWriteRequest, bool) { + // Control and RST_STREAM frames first. + if !ws.control.empty() { + return ws.control.shift(), true + } + + // On the next Pop(), we want to prioritize incremental if we prioritized + // non-incremental request of the same urgency this time. Vice-versa. + // i.e. when there are incremental and non-incremental requests at the same + // priority, we give 50% of our bandwidth to the incremental ones in + // aggregate and 50% to the first non-incremental one (since + // non-incremental streams do not use round-robin writes). + ws.prioritizeIncremental = !ws.prioritizeIncremental + + // Always prioritize lowest u (i.e. highest urgency level). + for u := range ws.heads { + for i := range ws.heads[u] { + // When we want to prioritize incremental, we try to pop i=true + // first before i=false when u is the same. + if ws.prioritizeIncremental { + i = (i + 1) % 2 + } + q := ws.heads[u][i] + if q == nil { + continue + } + for { + if wr, ok := q.consume(math.MaxInt32); ok { + if i == 1 { + // For incremental streams, we update head to q.next so + // we can round-robin between multiple streams that can + // immediately benefit from partial writes. + ws.heads[u][i] = q.next + } else { + // For non-incremental streams, we try to finish one to + // completion rather than doing round-robin. However, + // we update head here so that if q.consume() is !ok + // (e.g. the stream has no more frame to consume), head + // is updated to the next q that has frames to consume + // on future iterations. This way, we do not prioritize + // writing to unavailable stream on next Pop() calls, + // preventing head-of-line blocking. + ws.heads[u][i] = q + } + return wr, true + } + q = q.next + if q == ws.heads[u][i] { + break + } + } + + } + } + return http2FrameWriteRequest{}, false +} + // NewRandomWriteScheduler constructs a WriteScheduler that ignores HTTP/2 // priorities. Control frames like SETTINGS and PING are written before DATA // frames, but if no control frames are queued and multiple streams have queued @@ -11622,7 +11859,7 @@ type http2roundRobinWriteScheduler struct { } // newRoundRobinWriteScheduler constructs a new write scheduler. -// The round robin scheduler priorizes control frames +// The round robin scheduler prioritizes control frames // like SETTINGS and PING over DATA frames. // When there are no control frames to send, it performs a round-robin // selection from the ready streams. diff --git a/src/net/http/http2_test.go b/src/net/http/http2_test.go deleted file mode 100644 index 7841b05b593400..00000000000000 --- a/src/net/http/http2_test.go +++ /dev/null @@ -1,12 +0,0 @@ -// 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. - -//go:build !nethttpomithttp2 - -package http - -func init() { - // Disable HTTP/2 internal channel pooling which interferes with synctest. - http2inTests = true -} diff --git a/src/net/http/internal/httpcommon/httpcommon.go b/src/net/http/internal/httpcommon/httpcommon.go index 5e0c24035aba20..e3f8ec79e094d3 100644 --- a/src/net/http/internal/httpcommon/httpcommon.go +++ b/src/net/http/internal/httpcommon/httpcommon.go @@ -202,7 +202,7 @@ type EncodeHeadersParam struct { DefaultUserAgent string } -// EncodeHeadersParam is the result of EncodeHeaders. +// EncodeHeadersResult is the result of EncodeHeaders. type EncodeHeadersResult struct { HasBody bool HasTrailers bool @@ -550,7 +550,7 @@ type ServerRequestResult struct { // If the request should be rejected, this is a short string suitable for passing // to the http2 package's CountError function. - // It might be a bit odd to return errors this way rather than returing an error, + // It might be a bit odd to return errors this way rather than returning an error, // but this ensures we don't forget to include a CountError reason. InvalidReason string } diff --git a/src/net/http/socks_bundle.go b/src/net/http/socks_bundle.go index 776b03d941a405..862e5fa2a5165d 100644 --- a/src/net/http/socks_bundle.go +++ b/src/net/http/socks_bundle.go @@ -453,7 +453,7 @@ func (up *socksUsernamePassword) Authenticate(ctx context.Context, rw io.ReadWri b = append(b, up.Username...) b = append(b, byte(len(up.Password))) b = append(b, up.Password...) - // TODO(mikio): handle IO deadlines and cancelation if + // TODO(mikio): handle IO deadlines and cancellation if // necessary if _, err := rw.Write(b); err != nil { return err diff --git a/src/vendor/golang.org/x/net/nettest/conntest.go b/src/vendor/golang.org/x/net/nettest/conntest.go index 4297d408c0477c..8b98dfe21c5d30 100644 --- a/src/vendor/golang.org/x/net/nettest/conntest.go +++ b/src/vendor/golang.org/x/net/nettest/conntest.go @@ -142,7 +142,7 @@ func testPingPong(t *testing.T, c1, c2 net.Conn) { } // testRacyRead tests that it is safe to mutate the input Read buffer -// immediately after cancelation has occurred. +// immediately after cancellation has occurred. func testRacyRead(t *testing.T, c1, c2 net.Conn) { go chunkedCopy(c2, rand.New(rand.NewSource(0))) @@ -170,7 +170,7 @@ func testRacyRead(t *testing.T, c1, c2 net.Conn) { } // testRacyWrite tests that it is safe to mutate the input Write buffer -// immediately after cancelation has occurred. +// immediately after cancellation has occurred. func testRacyWrite(t *testing.T, c1, c2 net.Conn) { go chunkedCopy(io.Discard, c2) @@ -318,7 +318,7 @@ func testCloseTimeout(t *testing.T, c1, c2 net.Conn) { defer wg.Wait() wg.Add(3) - // Test for cancelation upon connection closure. + // Test for cancellation upon connection closure. c1.SetDeadline(neverTimeout) go func() { defer wg.Done() diff --git a/src/vendor/modules.txt b/src/vendor/modules.txt index cf5c27c74579d0..a2a0c0b3e85f23 100644 --- a/src/vendor/modules.txt +++ b/src/vendor/modules.txt @@ -6,7 +6,7 @@ golang.org/x/crypto/cryptobyte golang.org/x/crypto/cryptobyte/asn1 golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 -# golang.org/x/net v0.44.0 +# golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f ## explicit; go 1.24.0 golang.org/x/net/dns/dnsmessage golang.org/x/net/http/httpguts From 53845004d647e16b3de7c74e50cffaca77e028e9 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 2 Oct 2025 09:42:57 -0700 Subject: [PATCH 40/63] net/http/httputil: deprecate ReverseProxy.Director The Director function has been superseded by Rewrite. Rewrite avoids fundamental security issues with hop-by-hop header handling in the Director API and has better default handling of X-Forwarded-* headers. Fixes #73161 Change-Id: Iadaf3070e0082458f79fb892ade51cb7ce832802 Reviewed-on: https://go-review.googlesource.com/c/go/+/708615 Reviewed-by: Nicholas Husin LUCI-TryBot-Result: Go LUCI Reviewed-by: Nicholas Husin --- api/next/73161.txt | 1 + .../99-minor/net/http/httputil/73161.md | 11 ++ src/net/http/httputil/reverseproxy.go | 116 +++++++++++++----- 3 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 api/next/73161.txt create mode 100644 doc/next/6-stdlib/99-minor/net/http/httputil/73161.md diff --git a/api/next/73161.txt b/api/next/73161.txt new file mode 100644 index 00000000000000..86526b597a9463 --- /dev/null +++ b/api/next/73161.txt @@ -0,0 +1 @@ +pkg net/http/httputil, type ReverseProxy struct, Director //deprecated #73161 diff --git a/doc/next/6-stdlib/99-minor/net/http/httputil/73161.md b/doc/next/6-stdlib/99-minor/net/http/httputil/73161.md new file mode 100644 index 00000000000000..f6318f85534746 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/net/http/httputil/73161.md @@ -0,0 +1,11 @@ +The [ReverseProxy.Director] configuration field is deprecated +in favor of [ReverseProxy.Rewrite]. + +A malicious client can remove headers added by a `Director` function +by designating those headers as hop-by-hop. Since there is no way to address +this problem within the scope of the `Director` API, we added a new +`Rewrite` hook in Go 1.20. `Rewrite` hooks are provided with both the +unmodified inbound request received by the proxy and the outbound request +which will be sent by the proxy. + +Since the `Director` hook is fundamentally unsafe, we are now deprecating it. diff --git a/src/net/http/httputil/reverseproxy.go b/src/net/http/httputil/reverseproxy.go index 6ed4930727404b..9d8784cd2bc7a9 100644 --- a/src/net/http/httputil/reverseproxy.go +++ b/src/net/http/httputil/reverseproxy.go @@ -133,36 +133,6 @@ type ReverseProxy struct { // At most one of Rewrite or Director may be set. Rewrite func(*ProxyRequest) - // Director is a function which modifies - // the request into a new request to be sent - // using Transport. Its response is then copied - // back to the original client unmodified. - // Director must not access the provided Request - // after returning. - // - // By default, the X-Forwarded-For header is set to the - // value of the client IP address. If an X-Forwarded-For - // header already exists, the client IP is appended to the - // existing values. As a special case, if the header - // exists in the Request.Header map but has a nil value - // (such as when set by the Director func), the X-Forwarded-For - // header is not modified. - // - // To prevent IP spoofing, be sure to delete any pre-existing - // X-Forwarded-For header coming from the client or - // an untrusted proxy. - // - // Hop-by-hop headers are removed from the request after - // Director returns, which can remove headers added by - // Director. Use a Rewrite function instead to ensure - // modifications to the request are preserved. - // - // Unparsable query parameters are removed from the outbound - // request if Request.Form is set after Director returns. - // - // At most one of Rewrite or Director may be set. - Director func(*http.Request) - // The transport used to perform proxy requests. // If nil, http.DefaultTransport is used. Transport http.RoundTripper @@ -210,6 +180,88 @@ type ReverseProxy struct { // If nil, the default is to log the provided error and return // a 502 Status Bad Gateway response. ErrorHandler func(http.ResponseWriter, *http.Request, error) + + // Director is deprecated. Use Rewrite instead. + // + // This function is insecure: + // + // - Hop-by-hop headers are removed from the request after Director + // returns, which can remove headers added by Director. + // A client can designate headers as hop-by-hop by listing them + // in the Connection header, so this permits a malicious client + // to remove any headers that may be added by Director. + // + // - X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto + // headers in inbound requests are preserved by default, + // which can permit IP spoofing if the Director function is + // not careful to remove these headers. + // + // Rewrite addresses these issues. + // + // As an example of converting a Director function to Rewrite: + // + // // ReverseProxy with a Director function. + // proxy := &httputil.ReverseProxy{ + // Director: func(req *http.Request) { + // req.URL.Scheme = "https" + // req.URL.Host = proxyHost + // + // // A malicious client can remove this header. + // req.Header.Set("Some-Header", "some-header-value") + // + // // X-Forwarded-* headers sent by the client are preserved, + // // since Director did not remove them. + // }, + // } + // + // // ReverseProxy with a Rewrite function. + // proxy := &httputil.ReverseProxy{ + // Rewrite: func(preq *httputil.ProxyRequest) { + // // See also ProxyRequest.SetURL. + // preq.Out.URL.Scheme = "https" + // preq.Out.URL.Host = proxyHost + // + // // This header cannot be affected by a malicious client. + // preq.Out.Header.Set("Some-Header", "some-header-value") + // + // // X-Forwarded- headers sent by the client have been + // // removed from preq.Out. + // // ProxyRequest.SetXForwarded optionally adds new ones. + // preq.SetXForwarded() + // }, + // } + // + // Director is a function which modifies + // the request into a new request to be sent + // using Transport. Its response is then copied + // back to the original client unmodified. + // Director must not access the provided Request + // after returning. + // + // By default, the X-Forwarded-For header is set to the + // value of the client IP address. If an X-Forwarded-For + // header already exists, the client IP is appended to the + // existing values. As a special case, if the header + // exists in the Request.Header map but has a nil value + // (such as when set by the Director func), the X-Forwarded-For + // header is not modified. + // + // To prevent IP spoofing, be sure to delete any pre-existing + // X-Forwarded-For header coming from the client or + // an untrusted proxy. + // + // Hop-by-hop headers are removed from the request after + // Director returns, which can remove headers added by + // Director. Use a Rewrite function instead to ensure + // modifications to the request are preserved. + // + // Unparsable query parameters are removed from the outbound + // request if Request.Form is set after Director returns. + // + // At most one of Rewrite or Director may be set. + // + // Deprecated: Use Rewrite instead. + Director func(*http.Request) } // A BufferPool is an interface for getting and returning temporary @@ -259,6 +311,10 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) { // // NewSingleHostReverseProxy does not rewrite the Host header. // +// For backwards compatibility reasons, NewSingleHostReverseProxy +// returns a ReverseProxy using the deprecated Director function. +// This proxy preserves X-Forwarded-* headers sent by the client. +// // To customize the ReverseProxy behavior beyond what // NewSingleHostReverseProxy provides, use ReverseProxy directly // with a Rewrite function. The ProxyRequest SetURL method From d5b950399de01a0e28eeb48d2c8474db4aad0e8a Mon Sep 17 00:00:00 2001 From: Tim Cooijmans Date: Tue, 30 Sep 2025 21:53:11 +0000 Subject: [PATCH 41/63] cmd/cgo: fix unaligned arguments typedmemmove crash on iOS Irregularly typedmemmove and bulkBarrierPreWrite crashes on unaligned arguments. By aligning the arguments this is fixed. Fixes #46893 Change-Id: I7beb9fdc31053fcb71bee6c6cb906dea31718c56 GitHub-Last-Rev: 46ae8b96889644aab60ea4284cf447a740354c6a GitHub-Pull-Request: golang/go#74868 Reviewed-on: https://go-review.googlesource.com/c/go/+/692935 Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI Reviewed-by: Keith Randall Reviewed-by: Keith Randall --- src/cmd/cgo/internal/testout/out_test.go | 144 ++++++++++++++++++ .../cgo/internal/testout/testdata/aligned.go | 63 ++++++++ src/cmd/cgo/out.go | 13 +- 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/cmd/cgo/internal/testout/out_test.go create mode 100644 src/cmd/cgo/internal/testout/testdata/aligned.go diff --git a/src/cmd/cgo/internal/testout/out_test.go b/src/cmd/cgo/internal/testout/out_test.go new file mode 100644 index 00000000000000..81dfa365871372 --- /dev/null +++ b/src/cmd/cgo/internal/testout/out_test.go @@ -0,0 +1,144 @@ +// 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 out_test + +import ( + "bufio" + "bytes" + "fmt" + "internal/testenv" + "internal/goarch" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" +) + +type methodAlign struct { + Method string + Align int +} + +var wantAligns = map[string]int{ + "ReturnEmpty": 1, + "ReturnOnlyUint8": 1, + "ReturnOnlyUint16": 2, + "ReturnOnlyUint32": 4, + "ReturnOnlyUint64": goarch.PtrSize, + "ReturnOnlyInt": goarch.PtrSize, + "ReturnOnlyPtr": goarch.PtrSize, + "ReturnByteSlice": goarch.PtrSize, + "ReturnString": goarch.PtrSize, + "InputAndReturnUint8": 1, + "MixedTypes": goarch.PtrSize, +} + +// TestAligned tests that the generated _cgo_export.c file has the wanted +// align attributes for struct types used as arguments or results of +// //exported functions. +func TestAligned(t *testing.T) { + testenv.MustHaveGoRun(t) + testenv.MustHaveCGO(t) + + testdata, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + objDir := t.TempDir() + + cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "cgo", + "-objdir", objDir, + filepath.Join(testdata, "aligned.go")) + cmd.Stderr = new(bytes.Buffer) + + err = cmd.Run() + if err != nil { + t.Fatalf("%#q: %v\n%s", cmd, err, cmd.Stderr) + } + + haveAligns, err := parseAlign(filepath.Join(objDir, "_cgo_export.c")) + if err != nil { + t.Fatal(err) + } + + // Check that we have all the wanted methods + if len(haveAligns) != len(wantAligns) { + t.Fatalf("have %d methods with aligned, want %d", len(haveAligns), len(wantAligns)) + } + + for i := range haveAligns { + method := haveAligns[i].Method + haveAlign := haveAligns[i].Align + + wantAlign, ok := wantAligns[method] + if !ok { + t.Errorf("method %s: have aligned %d, want missing entry", method, haveAlign) + } else if haveAlign != wantAlign { + t.Errorf("method %s: have aligned %d, want %d", method, haveAlign, wantAlign) + } + } +} + +func parseAlign(filename string) ([]methodAlign, error) { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + var results []methodAlign + scanner := bufio.NewScanner(file) + + // Regex to match function declarations like "struct MethodName_return MethodName(" + funcRegex := regexp.MustCompile(`^struct\s+(\w+)_return\s+(\w+)\(`) + // Regex to match simple function declarations like "GoSlice MethodName(" + simpleFuncRegex := regexp.MustCompile(`^Go\w+\s+(\w+)\(`) + // Regex to match void-returning exported functions like "void ReturnEmpty(" + voidFuncRegex := regexp.MustCompile(`^void\s+(\w+)\(`) + // Regex to match align attributes like "__attribute__((aligned(8)))" + alignRegex := regexp.MustCompile(`__attribute__\(\(aligned\((\d+)\)\)\)`) + + var currentMethod string + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Check if this line declares a function with struct return type + if matches := funcRegex.FindStringSubmatch(line); matches != nil { + currentMethod = matches[2] // Extract the method name + } else if matches := simpleFuncRegex.FindStringSubmatch(line); matches != nil { + // Check if this line declares a function with simple return type (like GoSlice) + currentMethod = matches[1] // Extract the method name + } else if matches := voidFuncRegex.FindStringSubmatch(line); matches != nil { + // Check if this line declares a void-returning function + currentMethod = matches[1] // Extract the method name + } + + // Check if this line contains align information + if alignMatches := alignRegex.FindStringSubmatch(line); alignMatches != nil && currentMethod != "" { + alignStr := alignMatches[1] + align, err := strconv.Atoi(alignStr) + if err != nil { + // Skip this entry if we can't parse the align as integer + currentMethod = "" + continue + } + results = append(results, methodAlign{ + Method: currentMethod, + Align: align, + }) + currentMethod = "" // Reset for next method + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + return results, nil +} diff --git a/src/cmd/cgo/internal/testout/testdata/aligned.go b/src/cmd/cgo/internal/testout/testdata/aligned.go new file mode 100644 index 00000000000000..cea6f2889a0cad --- /dev/null +++ b/src/cmd/cgo/internal/testout/testdata/aligned.go @@ -0,0 +1,63 @@ +// 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 + +import "C" + +//export ReturnEmpty +func ReturnEmpty() { + return +} + +//export ReturnOnlyUint8 +func ReturnOnlyUint8() (uint8, uint8, uint8) { + return 1, 2, 3 +} + +//export ReturnOnlyUint16 +func ReturnOnlyUint16() (uint16, uint16, uint16) { + return 1, 2, 3 +} + +//export ReturnOnlyUint32 +func ReturnOnlyUint32() (uint32, uint32, uint32) { + return 1, 2, 3 +} + +//export ReturnOnlyUint64 +func ReturnOnlyUint64() (uint64, uint64, uint64) { + return 1, 2, 3 +} + +//export ReturnOnlyInt +func ReturnOnlyInt() (int, int, int) { + return 1, 2, 3 +} + +//export ReturnOnlyPtr +func ReturnOnlyPtr() (*int, *int, *int) { + a, b, c := 1, 2, 3 + return &a, &b, &c +} + +//export ReturnString +func ReturnString() string { + return "hello" +} + +//export ReturnByteSlice +func ReturnByteSlice() []byte { + return []byte{1, 2, 3} +} + +//export InputAndReturnUint8 +func InputAndReturnUint8(a, b, c uint8) (uint8, uint8, uint8) { + return a, b, c +} + +//export MixedTypes +func MixedTypes(a uint8, b uint16, c uint32, d uint64, e int, f *int) (uint8, uint16, uint32, uint64, int, *int) { + return a, b, c, d, e, f +} diff --git a/src/cmd/cgo/out.go b/src/cmd/cgo/out.go index 622d35ac7b3bab..a2bcdf89c5ad44 100644 --- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -949,6 +949,8 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) { fmt.Fprintf(gotype, "struct {\n") off := int64(0) npad := 0 + // the align is at least 1 (for char) + maxAlign := int64(1) argField := func(typ ast.Expr, namePat string, args ...interface{}) { name := fmt.Sprintf(namePat, args...) t := p.cgoType(typ) @@ -963,6 +965,11 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) { noSourceConf.Fprint(gotype, fset, typ) fmt.Fprintf(gotype, "\n") off += t.Size + // keep track of the maximum alignment among all fields + // so that we can align the struct correctly + if t.Align > maxAlign { + maxAlign = t.Align + } } if fn.Recv != nil { argField(fn.Recv.List[0].Type, "recv") @@ -1047,7 +1054,11 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) { // string.h for memset, and is also robust to C++ // types with constructors. Both GCC and LLVM optimize // this into just zeroing _cgo_a. - fmt.Fprintf(fgcc, "\ttypedef %s %v _cgo_argtype;\n", ctype.String(), p.packedAttribute()) + // + // The struct should be aligned to the maximum alignment + // of any of its fields. This to avoid alignment + // issues. + fmt.Fprintf(fgcc, "\ttypedef %s %v __attribute__((aligned(%d))) _cgo_argtype;\n", ctype.String(), p.packedAttribute(), maxAlign) fmt.Fprintf(fgcc, "\tstatic _cgo_argtype _cgo_zero;\n") fmt.Fprintf(fgcc, "\t_cgo_argtype _cgo_a = _cgo_zero;\n") if gccResult != "void" && (len(fntype.Results.List) > 1 || len(fntype.Results.List[0].Names) > 1) { From adce7f196e6ac6d22e9bc851efea5f3ab650947c Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Thu, 2 Oct 2025 18:24:12 -0400 Subject: [PATCH 42/63] cmd/link: support .def file with MSVC clang toolchain lld-link supports .def file, but requires a "-def:" (or "/def:") flag. (MinGW linker, on the other hand, requires no flag.) Pass the flag when using MSVC-based toolchain. CL originally authored by Chressie Himpel. Change-Id: I8c327ab48d36b0bcbb1d127cff544ffdb06be38e Reviewed-on: https://go-review.googlesource.com/c/go/+/708716 LUCI-TryBot-Result: Go LUCI Reviewed-by: Chressie Himpel --- src/cmd/link/internal/ld/lib.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cmd/link/internal/ld/lib.go b/src/cmd/link/internal/ld/lib.go index 79d3d37835e9ad..2c861129b52f9a 100644 --- a/src/cmd/link/internal/ld/lib.go +++ b/src/cmd/link/internal/ld/lib.go @@ -1787,9 +1787,13 @@ func (ctxt *Link) hostlink() { case ctxt.IsAIX(): fileName := xcoffCreateExportFile(ctxt) argv = append(argv, "-Wl,-bE:"+fileName) - case ctxt.IsWindows() && !slices.Contains(flagExtldflags, "-Wl,--export-all-symbols"): + case ctxt.IsWindows() && !slices.Contains(flagExtldflags, wlPrefix+"export-all-symbols"): fileName := peCreateExportFile(ctxt, filepath.Base(outopt)) - argv = append(argv, fileName) + prefix := "" + if isMSVC { + prefix = "-Wl,-def:" + } + argv = append(argv, prefix+fileName) } const unusedArguments = "-Qunused-arguments" From 630799c6c957994a7720b5d0b6bdd10f3acfc605 Mon Sep 17 00:00:00 2001 From: Daniel McCarney Date: Mon, 30 Jun 2025 12:45:36 -0400 Subject: [PATCH 43/63] crypto/tls: add flag to render HTML BoGo report Updates the BoGo test runner to add a `-bogo-html-report` flag. When provided, an HTML report is written to the flag argument path. The report shows the fail/pass/skip status of run tests and allows sorting/searching the output. Change-Id: I8c704a51fbb03500f4134ebfaba06248baa3ca2f Reviewed-on: https://go-review.googlesource.com/c/go/+/684955 Auto-Submit: Daniel McCarney Reviewed-by: Roland Shoemaker Reviewed-by: Carlos Amedee TryBot-Bypass: Daniel McCarney Commit-Queue: Carlos Amedee --- src/crypto/tls/bogo_shim_test.go | 152 ++++++++++++++++++++++++++++++- src/crypto/tls/handshake_test.go | 1 + 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/crypto/tls/bogo_shim_test.go b/src/crypto/tls/bogo_shim_test.go index 7cab568db80953..f3f19b34e9e4cc 100644 --- a/src/crypto/tls/bogo_shim_test.go +++ b/src/crypto/tls/bogo_shim_test.go @@ -13,6 +13,7 @@ import ( "encoding/pem" "flag" "fmt" + "html/template" "internal/byteorder" "internal/testenv" "io" @@ -25,10 +26,13 @@ import ( "strconv" "strings" "testing" + "time" "golang.org/x/crypto/cryptobyte" ) +const boringsslModVer = "v0.0.0-20250620172916-f51d8b099832" + var ( port = flag.String("port", "", "") server = flag.Bool("server", false, "") @@ -557,7 +561,6 @@ func TestBogoSuite(t *testing.T) { if *bogoLocalDir != "" { bogoDir = *bogoLocalDir } else { - const boringsslModVer = "v0.0.0-20250620172916-f51d8b099832" bogoDir = cryptotest.FetchModule(t, "boringssl.googlesource.com/boringssl.git", boringsslModVer) } @@ -606,6 +609,12 @@ func TestBogoSuite(t *testing.T) { t.Fatalf("failed to parse results JSON: %s", err) } + if *bogoReport != "" { + if err := generateReport(results, *bogoReport); err != nil { + t.Fatalf("failed to generate report: %v", err) + } + } + // assertResults contains test results we want to make sure // are present in the output. They are only checked if -bogo-filter // was not passed. @@ -655,6 +664,23 @@ func TestBogoSuite(t *testing.T) { } } +func generateReport(results bogoResults, outPath string) error { + data := reportData{ + Results: results, + Timestamp: time.Unix(int64(results.SecondsSinceEpoch), 0).Format("2006-01-02 15:04:05"), + Revision: boringsslModVer, + } + + tmpl := template.Must(template.New("report").Parse(reportTemplate)) + file, err := os.Create(outPath) + if err != nil { + return err + } + defer file.Close() + + return tmpl.Execute(file, data) +} + // bogoResults is a copy of boringssl.googlesource.com/boringssl/testresults.Results type bogoResults struct { Version int `json:"version"` @@ -669,3 +695,127 @@ type bogoResults struct { Error string `json:"error,omitempty"` } `json:"tests"` } + +type reportData struct { + Results bogoResults + SkipReasons map[string]string + Timestamp string + Revision string +} + +const reportTemplate = ` + + + + BoGo Results Report + + + +

BoGo Results Report

+ +
+ Generated: {{.Timestamp}} | BoGo Revision: {{.Revision}}
+ {{range $status, $count := .Results.NumFailuresByType}} + {{$status}}: {{$count}} | + {{end}} +
+ +
+ + +
+ + + + + + + + + + + + + {{range $name, $test := .Results.Tests}} + + + + + + + + {{end}} + +
Test NameStatusActualExpectedError
{{$name}}{{$test.Actual}}{{$test.Actual}}{{$test.Expected}}{{$test.Error}}
+ + + + +` diff --git a/src/crypto/tls/handshake_test.go b/src/crypto/tls/handshake_test.go index ea8ac6fc837ae7..13f1bb1dd50a19 100644 --- a/src/crypto/tls/handshake_test.go +++ b/src/crypto/tls/handshake_test.go @@ -47,6 +47,7 @@ var ( bogoMode = flag.Bool("bogo-mode", false, "Enabled bogo shim mode, ignore everything else") bogoFilter = flag.String("bogo-filter", "", "BoGo test filter") bogoLocalDir = flag.String("bogo-local-dir", "", "Local BoGo to use, instead of fetching from source") + bogoReport = flag.String("bogo-html-report", "", "File path to render an HTML report with BoGo results") ) func runTestAndUpdateIfNeeded(t *testing.T, name string, run func(t *testing.T, update bool), wait bool) { From a7917eed70c63840b2c59748c52ef9ff11fecf38 Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Mon, 17 Mar 2025 11:45:52 -0400 Subject: [PATCH 44/63] internal/buildcfg: enable specializedmalloc experiment Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64_c2s16-perf_vs_parent,gotip-linux-amd64_c3h88-perf_vs_parent,gotip-linux-arm64_c4ah72-perf_vs_parent,gotip-linux-arm64_c4as16-perf_vs_parent Change-Id: I6a6a6964c4c596bbf4f072b5a44a34c3ce4f6541 Reviewed-on: https://go-review.googlesource.com/c/go/+/696536 Reviewed-by: Michael Matloob LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek --- src/internal/buildcfg/exp.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/internal/buildcfg/exp.go b/src/internal/buildcfg/exp.go index 707776b374b0d4..d06913d9a7f25a 100644 --- a/src/internal/buildcfg/exp.go +++ b/src/internal/buildcfg/exp.go @@ -79,10 +79,11 @@ func ParseGOEXPERIMENT(goos, goarch, goexp string) (*ExperimentFlags, error) { dwarf5Supported := (goos != "darwin" && goos != "ios" && goos != "aix") baseline := goexperiment.Flags{ - RegabiWrappers: regabiSupported, - RegabiArgs: regabiSupported, - Dwarf5: dwarf5Supported, - RandomizedHeapBase64: true, + RegabiWrappers: regabiSupported, + RegabiArgs: regabiSupported, + Dwarf5: dwarf5Supported, + RandomizedHeapBase64: true, + SizeSpecializedMalloc: true, } // Start with the statically enabled set of experiments. From c54dc1418b6fbff4176aaaffcc9fab6f1ad631a6 Mon Sep 17 00:00:00 2001 From: matloob Date: Wed, 1 Oct 2025 12:06:14 -0400 Subject: [PATCH 45/63] runtime: support valgrind (but not asan) in specialized malloc functions We're adding this so that the compiler doesn't need to know about valgrind since it's just implemented using a build tag. Change-Id: I6a6a696452b0379caceca2ae4e49195016f7a90d Reviewed-on: https://go-review.googlesource.com/c/go/+/708296 Reviewed-by: Michael Matloob LUCI-TryBot-Result: Go LUCI Auto-Submit: Michael Matloob Reviewed-by: Michael Knyszek --- src/runtime/malloc_generated.go | 324 ++++++++++++++++++++++++++++++++ src/runtime/malloc_stubs.go | 9 + 2 files changed, 333 insertions(+) diff --git a/src/runtime/malloc_generated.go b/src/runtime/malloc_generated.go index 600048c67557b1..2215dbaddb2e1c 100644 --- a/src/runtime/malloc_generated.go +++ b/src/runtime/malloc_generated.go @@ -150,6 +150,10 @@ func mallocgcSmallScanNoHeaderSC1(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -304,6 +308,10 @@ func mallocgcSmallScanNoHeaderSC2(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -458,6 +466,10 @@ func mallocgcSmallScanNoHeaderSC3(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -612,6 +624,10 @@ func mallocgcSmallScanNoHeaderSC4(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -766,6 +782,10 @@ func mallocgcSmallScanNoHeaderSC5(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -920,6 +940,10 @@ func mallocgcSmallScanNoHeaderSC6(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1074,6 +1098,10 @@ func mallocgcSmallScanNoHeaderSC7(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1228,6 +1256,10 @@ func mallocgcSmallScanNoHeaderSC8(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1382,6 +1414,10 @@ func mallocgcSmallScanNoHeaderSC9(size uintptr, typ *_type, needzero bool) unsaf gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1536,6 +1572,10 @@ func mallocgcSmallScanNoHeaderSC10(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1690,6 +1730,10 @@ func mallocgcSmallScanNoHeaderSC11(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1844,6 +1888,10 @@ func mallocgcSmallScanNoHeaderSC12(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -1998,6 +2046,10 @@ func mallocgcSmallScanNoHeaderSC13(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -2152,6 +2204,10 @@ func mallocgcSmallScanNoHeaderSC14(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -2306,6 +2362,10 @@ func mallocgcSmallScanNoHeaderSC15(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -2460,6 +2520,10 @@ func mallocgcSmallScanNoHeaderSC16(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -2614,6 +2678,10 @@ func mallocgcSmallScanNoHeaderSC17(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -2768,6 +2836,10 @@ func mallocgcSmallScanNoHeaderSC18(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -2922,6 +2994,10 @@ func mallocgcSmallScanNoHeaderSC19(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -3076,6 +3152,10 @@ func mallocgcSmallScanNoHeaderSC20(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -3230,6 +3310,10 @@ func mallocgcSmallScanNoHeaderSC21(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -3384,6 +3468,10 @@ func mallocgcSmallScanNoHeaderSC22(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -3538,6 +3626,10 @@ func mallocgcSmallScanNoHeaderSC23(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -3692,6 +3784,10 @@ func mallocgcSmallScanNoHeaderSC24(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -3846,6 +3942,10 @@ func mallocgcSmallScanNoHeaderSC25(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4000,6 +4100,10 @@ func mallocgcSmallScanNoHeaderSC26(size uintptr, typ *_type, needzero bool) unsa gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4064,6 +4168,10 @@ func mallocTiny1(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4142,6 +4250,10 @@ func mallocTiny1(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4206,6 +4318,10 @@ func mallocTiny2(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4284,6 +4400,10 @@ func mallocTiny2(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4348,6 +4468,10 @@ func mallocTiny3(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4426,6 +4550,10 @@ func mallocTiny3(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4490,6 +4618,10 @@ func mallocTiny4(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4568,6 +4700,10 @@ func mallocTiny4(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4632,6 +4768,10 @@ func mallocTiny5(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4710,6 +4850,10 @@ func mallocTiny5(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4774,6 +4918,10 @@ func mallocTiny6(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4852,6 +5000,10 @@ func mallocTiny6(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4916,6 +5068,10 @@ func mallocTiny7(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -4994,6 +5150,10 @@ func mallocTiny7(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5058,6 +5218,10 @@ func mallocTiny8(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5136,6 +5300,10 @@ func mallocTiny8(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5200,6 +5368,10 @@ func mallocTiny9(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5278,6 +5450,10 @@ func mallocTiny9(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5342,6 +5518,10 @@ func mallocTiny10(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5420,6 +5600,10 @@ func mallocTiny10(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5484,6 +5668,10 @@ func mallocTiny11(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5562,6 +5750,10 @@ func mallocTiny11(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5626,6 +5818,10 @@ func mallocTiny12(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5704,6 +5900,10 @@ func mallocTiny12(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5768,6 +5968,10 @@ func mallocTiny13(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5846,6 +6050,10 @@ func mallocTiny13(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5910,6 +6118,10 @@ func mallocTiny14(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -5988,6 +6200,10 @@ func mallocTiny14(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6052,6 +6268,10 @@ func mallocTiny15(size uintptr, typ *_type, needzero bool) unsafe.Pointer { const elemsize = 0 { + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6130,6 +6350,10 @@ func mallocTiny15(size uintptr, typ *_type, needzero bool) unsafe.Pointer { x = add(x, elemsize-constsize) } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6223,6 +6447,10 @@ func mallocgcSmallNoScanSC2(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6316,6 +6544,10 @@ func mallocgcSmallNoScanSC3(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6409,6 +6641,10 @@ func mallocgcSmallNoScanSC4(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6502,6 +6738,10 @@ func mallocgcSmallNoScanSC5(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6595,6 +6835,10 @@ func mallocgcSmallNoScanSC6(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6688,6 +6932,10 @@ func mallocgcSmallNoScanSC7(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6781,6 +7029,10 @@ func mallocgcSmallNoScanSC8(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6874,6 +7126,10 @@ func mallocgcSmallNoScanSC9(size uintptr, typ *_type, needzero bool) unsafe.Poin gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -6967,6 +7223,10 @@ func mallocgcSmallNoScanSC10(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7060,6 +7320,10 @@ func mallocgcSmallNoScanSC11(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7153,6 +7417,10 @@ func mallocgcSmallNoScanSC12(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7246,6 +7514,10 @@ func mallocgcSmallNoScanSC13(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7339,6 +7611,10 @@ func mallocgcSmallNoScanSC14(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7432,6 +7708,10 @@ func mallocgcSmallNoScanSC15(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7525,6 +7805,10 @@ func mallocgcSmallNoScanSC16(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7618,6 +7902,10 @@ func mallocgcSmallNoScanSC17(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7711,6 +7999,10 @@ func mallocgcSmallNoScanSC18(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7804,6 +8096,10 @@ func mallocgcSmallNoScanSC19(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7897,6 +8193,10 @@ func mallocgcSmallNoScanSC20(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -7990,6 +8290,10 @@ func mallocgcSmallNoScanSC21(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -8083,6 +8387,10 @@ func mallocgcSmallNoScanSC22(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -8176,6 +8484,10 @@ func mallocgcSmallNoScanSC23(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -8269,6 +8581,10 @@ func mallocgcSmallNoScanSC24(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -8362,6 +8678,10 @@ func mallocgcSmallNoScanSC25(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) @@ -8455,6 +8775,10 @@ func mallocgcSmallNoScanSC26(size uintptr, typ *_type, needzero bool) unsafe.Poi gcStart(t) } } + if valgrindenabled { + valgrindMalloc(x, size) + } + if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { assistG.gcAssistBytes -= int64(elemsize - size) diff --git a/src/runtime/malloc_stubs.go b/src/runtime/malloc_stubs.go index 7fd144418938fb..224746f3d41124 100644 --- a/src/runtime/malloc_stubs.go +++ b/src/runtime/malloc_stubs.go @@ -50,6 +50,8 @@ func mallocPanic(size uintptr, typ *_type, needzero bool) unsafe.Pointer { panic("not defined for sizeclass") } +// WARNING: mallocStub does not do any work for sanitizers so callers need +// to steer out of this codepath early if sanitizers are enabled. func mallocStub(size uintptr, typ *_type, needzero bool) unsafe.Pointer { if doubleCheckMalloc { if gcphase == _GCmarktermination { @@ -77,6 +79,13 @@ func mallocStub(size uintptr, typ *_type, needzero bool) unsafe.Pointer { // Actually do the allocation. x, elemsize := inlinedMalloc(size, typ, needzero) + // Notify valgrind, if enabled. + // To allow the compiler to not know about valgrind, we do valgrind instrumentation + // unlike the other sanitizers. + if valgrindenabled { + valgrindMalloc(x, size) + } + // Adjust our GC assist debt to account for internal fragmentation. if gcBlackenEnabled != 0 && elemsize != 0 { if assistG := getg().m.curg; assistG != nil { From ebb72bef44a0e125c7f900a04af6538e3c39dfc6 Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Fri, 20 Jun 2025 12:02:18 -0400 Subject: [PATCH 46/63] cmd/compile: don't treat devel compiler as a released compiler The compiler has a logic to print different messages on internal compiler error depending on whether this is a released version of Go. It hides the panic stack trace if it is a released version. It does this by checking the version and see if it has a "go" prefix. This includes all the released versions. However, for a non- released build, if there is no explicit version set, cmd/dist now sets the toolchain version as go1.X-devel_XXX, which makes it be treated as a released compiler, and causes the stack trace to be hidden. Change the logic to not match a devel compiler as a released compiler. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: I5d3b2101527212f825b6e4000b36030c4f83870b Reviewed-on: https://go-review.googlesource.com/c/go/+/682975 Reviewed-by: David Chase LUCI-TryBot-Result: Go LUCI Reviewed-on: https://go-review.googlesource.com/c/go/+/708855 Reviewed-by: Junyang Shao --- src/cmd/compile/internal/base/print.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/compile/internal/base/print.go b/src/cmd/compile/internal/base/print.go index 119f06fbc03351..9e3348c1ecca89 100644 --- a/src/cmd/compile/internal/base/print.go +++ b/src/cmd/compile/internal/base/print.go @@ -220,7 +220,7 @@ func FatalfAt(pos src.XPos, format string, args ...interface{}) { fmt.Printf("\n") // If this is a released compiler version, ask for a bug report. - if Debug.Panic == 0 && strings.HasPrefix(buildcfg.Version, "go") { + if Debug.Panic == 0 && strings.HasPrefix(buildcfg.Version, "go") && !strings.Contains(buildcfg.Version, "devel") { fmt.Printf("\n") fmt.Printf("Please file a bug report including a short program that triggers the error.\n") fmt.Printf("https://go.dev/issue/new\n") From ab043953cbd6e3cd262548710f35f05924aa8f32 Mon Sep 17 00:00:00 2001 From: David Chase Date: Wed, 2 Jul 2025 18:00:12 -0400 Subject: [PATCH 47/63] cmd/compile: minor tweak for race detector This makes the front-end a little bit less temp-happy when instrumenting, which repairs the "is it a constant?" test in the simd intrinsic conversion which is otherwise broken by race detection. Also, this will perhaps be better code. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: I84b7a45b7bff62bb2c9f9662466b50858d288645 Reviewed-on: https://go-review.googlesource.com/c/go/+/685637 LUCI-TryBot-Result: Go LUCI Reviewed-by: Junyang Shao Reviewed-by: Cherry Mui Reviewed-on: https://go-review.googlesource.com/c/go/+/708856 --- src/cmd/compile/internal/walk/walk.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/cmd/compile/internal/walk/walk.go b/src/cmd/compile/internal/walk/walk.go index a7d8182a747ce7..25add3d8043905 100644 --- a/src/cmd/compile/internal/walk/walk.go +++ b/src/cmd/compile/internal/walk/walk.go @@ -275,6 +275,15 @@ func backingArrayPtrLen(n ir.Node) (ptr, length ir.Node) { // function calls, which could clobber function call arguments/results // currently on the stack. func mayCall(n ir.Node) bool { + // This is intended to avoid putting constants + // into temporaries with the race detector (or other + // instrumentation) which interferes with simple + // "this is a constant" tests in ssagen. + // Also, it will generally lead to better code. + if n.Op() == ir.OLITERAL { + return false + } + // When instrumenting, any expression might require function calls. if base.Flag.Cfg.Instrumenting { return true From 10e796884905d23ab2419cc158769e8fdc73de4e Mon Sep 17 00:00:00 2001 From: Junyang Shao Date: Tue, 12 Aug 2025 16:53:44 +0000 Subject: [PATCH 48/63] cmd/compile: accounts rematerialize ops's output reginfo This CL implements the check for rematerializeable value's output regspec at its remateralization site. It has some potential problems, please see the TODO in regalloc.go. Fixes #70451. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: Ib624b967031776851136554719e939e9bf116b7c Reviewed-on: https://go-review.googlesource.com/c/go/+/695315 Reviewed-by: David Chase TryBot-Bypass: David Chase Reviewed-on: https://go-review.googlesource.com/c/go/+/708857 Reviewed-by: Junyang Shao LUCI-TryBot-Result: Go LUCI --- src/cmd/compile/internal/ssa/func.go | 1 + src/cmd/compile/internal/ssa/func_test.go | 5 ++++ src/cmd/compile/internal/ssa/regalloc.go | 23 +++++++++++++++++ src/cmd/compile/internal/ssa/regalloc_test.go | 25 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/src/cmd/compile/internal/ssa/func.go b/src/cmd/compile/internal/ssa/func.go index 5736f0b8126484..fc8cb3f2fef0af 100644 --- a/src/cmd/compile/internal/ssa/func.go +++ b/src/cmd/compile/internal/ssa/func.go @@ -102,6 +102,7 @@ func (c *Config) NewFunc(fe Frontend, cache *Cache) *Func { NamedValues: make(map[LocalSlot][]*Value), CanonicalLocalSlots: make(map[LocalSlot]*LocalSlot), CanonicalLocalSplits: make(map[LocalSlotSplitKey]*LocalSlot), + OwnAux: &AuxCall{}, } } diff --git a/src/cmd/compile/internal/ssa/func_test.go b/src/cmd/compile/internal/ssa/func_test.go index a1e639e0486b62..4639d674e145fa 100644 --- a/src/cmd/compile/internal/ssa/func_test.go +++ b/src/cmd/compile/internal/ssa/func_test.go @@ -250,6 +250,11 @@ func Exit(arg string) ctrl { return ctrl{BlockExit, arg, []string{}} } +// Ret specifies a BlockRet. +func Ret(arg string) ctrl { + return ctrl{BlockRet, arg, []string{}} +} + // Eq specifies a BlockAMD64EQ. func Eq(cond, sub, alt string) ctrl { return ctrl{BlockAMD64EQ, cond, []string{sub, alt}} diff --git a/src/cmd/compile/internal/ssa/regalloc.go b/src/cmd/compile/internal/ssa/regalloc.go index 1ce85a8f63b76a..56f9e550b2d439 100644 --- a/src/cmd/compile/internal/ssa/regalloc.go +++ b/src/cmd/compile/internal/ssa/regalloc.go @@ -609,6 +609,29 @@ func (s *regAllocState) allocValToReg(v *Value, mask regMask, nospill bool, pos } else if v.rematerializeable() { // Rematerialize instead of loading from the spill location. c = v.copyIntoWithXPos(s.curBlock, pos) + // We need to consider its output mask and potentially issue a Copy + // if there are register mask conflicts. + // This currently happens for the SIMD package only between GP and FP + // register. Because Intel's vector extension can put integer value into + // FP, which is seen as a vector. Example instruction: VPSLL[BWDQ] + // Because GP and FP masks do not overlap, mask & outputMask == 0 + // detects this situation thoroughly. + sourceMask := s.regspec(c).outputs[0].regs + if mask&sourceMask == 0 && !onWasmStack { + s.setOrig(c, v) + s.assignReg(s.allocReg(sourceMask, v), v, c) + // v.Type for the new OpCopy is likely wrong and it might delay the problem + // until ssa to asm lowering, which might need the types to generate the right + // assembly for OpCopy. For Intel's GP to FP move, it happens to be that + // MOV instruction has such a variant so it happens to be right. + // But it's unclear for other architectures or situations, and the problem + // might be exposed when the assembler sees illegal instructions. + // Right now make we still pick v.Type, because at least its size should be correct + // for the rematerialization case the amd64 SIMD package exposed. + // TODO: We might need to figure out a way to find the correct type or make + // the asm lowering use reg info only for OpCopy. + c = s.curBlock.NewValue1(pos, OpCopy, v.Type, c) + } } else { // Load v from its spill location. spill := s.makeSpill(v, s.curBlock) diff --git a/src/cmd/compile/internal/ssa/regalloc_test.go b/src/cmd/compile/internal/ssa/regalloc_test.go index 0f69b852d12971..79f94da0114f93 100644 --- a/src/cmd/compile/internal/ssa/regalloc_test.go +++ b/src/cmd/compile/internal/ssa/regalloc_test.go @@ -6,6 +6,7 @@ package ssa import ( "cmd/compile/internal/types" + "cmd/internal/obj/x86" "fmt" "testing" ) @@ -279,3 +280,27 @@ func numOps(b *Block, op Op) int { } return n } + +func TestRematerializeableRegCompatible(t *testing.T) { + c := testConfig(t) + f := c.Fun("entry", + Bloc("entry", + Valu("mem", OpInitMem, types.TypeMem, 0, nil), + Valu("x", OpAMD64MOVLconst, c.config.Types.Int32, 1, nil), + Valu("a", OpAMD64POR, c.config.Types.Float32, 0, nil, "x", "x"), + Valu("res", OpMakeResult, types.NewResults([]*types.Type{c.config.Types.Float32, types.TypeMem}), 0, nil, "a", "mem"), + Ret("res"), + ), + ) + regalloc(f.f) + checkFunc(f.f) + moveFound := false + for _, v := range f.f.Blocks[0].Values { + if v.Op == OpCopy && x86.REG_X0 <= v.Reg() && v.Reg() <= x86.REG_X31 { + moveFound = true + } + } + if !moveFound { + t.Errorf("Expects an Copy to be issued, but got: %+v", f.f) + } +} From ec70d1902355f10e0ab4788334b80db11ab69785 Mon Sep 17 00:00:00 2001 From: David Chase Date: Wed, 20 Aug 2025 12:29:02 -0400 Subject: [PATCH 49/63] cmd/compile: rewrite to elide Slicemask from len==c>0 slicing This might have been something that prove could be educated into figuring out, but this also works, and it also helps prove downstream. Adjusted the prove test, because this change moved a message. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: I5eabe639eff5db9cd9766a6a8666fdb4973829cb Reviewed-on: https://go-review.googlesource.com/c/go/+/697715 Commit-Queue: David Chase Reviewed-by: Cherry Mui TryBot-Bypass: David Chase Reviewed-on: https://go-review.googlesource.com/c/go/+/708858 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase Reviewed-by: Junyang Shao --- .../compile/internal/ssa/_gen/generic.rules | 4 + .../compile/internal/ssa/rewritegeneric.go | 87 +++++++++++++++++++ test/prove.go | 4 +- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/cmd/compile/internal/ssa/_gen/generic.rules b/src/cmd/compile/internal/ssa/_gen/generic.rules index b16aa473cd5961..6fdea7cc7a3cdc 100644 --- a/src/cmd/compile/internal/ssa/_gen/generic.rules +++ b/src/cmd/compile/internal/ssa/_gen/generic.rules @@ -989,6 +989,10 @@ (Const64 [0]) (Const64 [0])) +// Special rule to help constant slicing; len > 0 implies cap > 0 implies Slicemask is all 1 +(SliceMake (AddPtr x (And64 y (Slicemask _))) w:(Const64 [c]) z) && c > 0 => (SliceMake (AddPtr x y) w z) +(SliceMake (AddPtr x (And32 y (Slicemask _))) w:(Const32 [c]) z) && c > 0 => (SliceMake (AddPtr x y) w z) + // interface ops (ConstInterface) => (IMake diff --git a/src/cmd/compile/internal/ssa/rewritegeneric.go b/src/cmd/compile/internal/ssa/rewritegeneric.go index 5e0135be3a5be5..5720063f34b267 100644 --- a/src/cmd/compile/internal/ssa/rewritegeneric.go +++ b/src/cmd/compile/internal/ssa/rewritegeneric.go @@ -422,6 +422,8 @@ func rewriteValuegeneric(v *Value) bool { return rewriteValuegeneric_OpSliceCap(v) case OpSliceLen: return rewriteValuegeneric_OpSliceLen(v) + case OpSliceMake: + return rewriteValuegeneric_OpSliceMake(v) case OpSlicePtr: return rewriteValuegeneric_OpSlicePtr(v) case OpSlicemask: @@ -30514,6 +30516,91 @@ func rewriteValuegeneric_OpSliceLen(v *Value) bool { } return false } +func rewriteValuegeneric_OpSliceMake(v *Value) bool { + v_2 := v.Args[2] + v_1 := v.Args[1] + v_0 := v.Args[0] + b := v.Block + // match: (SliceMake (AddPtr x (And64 y (Slicemask _))) w:(Const64 [c]) z) + // cond: c > 0 + // result: (SliceMake (AddPtr x y) w z) + for { + if v_0.Op != OpAddPtr { + break + } + t := v_0.Type + _ = v_0.Args[1] + x := v_0.Args[0] + v_0_1 := v_0.Args[1] + if v_0_1.Op != OpAnd64 { + break + } + _ = v_0_1.Args[1] + v_0_1_0 := v_0_1.Args[0] + v_0_1_1 := v_0_1.Args[1] + for _i0 := 0; _i0 <= 1; _i0, v_0_1_0, v_0_1_1 = _i0+1, v_0_1_1, v_0_1_0 { + y := v_0_1_0 + if v_0_1_1.Op != OpSlicemask { + continue + } + w := v_1 + if w.Op != OpConst64 { + continue + } + c := auxIntToInt64(w.AuxInt) + z := v_2 + if !(c > 0) { + continue + } + v.reset(OpSliceMake) + v0 := b.NewValue0(v.Pos, OpAddPtr, t) + v0.AddArg2(x, y) + v.AddArg3(v0, w, z) + return true + } + break + } + // match: (SliceMake (AddPtr x (And32 y (Slicemask _))) w:(Const32 [c]) z) + // cond: c > 0 + // result: (SliceMake (AddPtr x y) w z) + for { + if v_0.Op != OpAddPtr { + break + } + t := v_0.Type + _ = v_0.Args[1] + x := v_0.Args[0] + v_0_1 := v_0.Args[1] + if v_0_1.Op != OpAnd32 { + break + } + _ = v_0_1.Args[1] + v_0_1_0 := v_0_1.Args[0] + v_0_1_1 := v_0_1.Args[1] + for _i0 := 0; _i0 <= 1; _i0, v_0_1_0, v_0_1_1 = _i0+1, v_0_1_1, v_0_1_0 { + y := v_0_1_0 + if v_0_1_1.Op != OpSlicemask { + continue + } + w := v_1 + if w.Op != OpConst32 { + continue + } + c := auxIntToInt32(w.AuxInt) + z := v_2 + if !(c > 0) { + continue + } + v.reset(OpSliceMake) + v0 := b.NewValue0(v.Pos, OpAddPtr, t) + v0.AddArg2(x, y) + v.AddArg3(v0, w, z) + return true + } + break + } + return false +} func rewriteValuegeneric_OpSlicePtr(v *Value) bool { v_0 := v.Args[0] // match: (SlicePtr (SliceMake (SlicePtr x) _ _)) diff --git a/test/prove.go b/test/prove.go index 70a27865cfd7c3..6d2bb0962be894 100644 --- a/test/prove.go +++ b/test/prove.go @@ -511,10 +511,10 @@ func f19() (e int64, err error) { func sm1(b []int, x int) { // Test constant argument to slicemask. - useSlice(b[2:8]) // ERROR "Proved slicemask not needed$" + useSlice(b[2:8]) // optimized away earlier by rewrite // Test non-constant argument with known limits. if cap(b) > 10 { - useSlice(b[2:]) + useSlice(b[2:]) // ERROR "Proved slicemask not needed$" } } From 1caa95acfa9d516eb3bc26292b5601bea25a4e79 Mon Sep 17 00:00:00 2001 From: David Chase Date: Wed, 3 Sep 2025 13:09:32 -0400 Subject: [PATCH 50/63] cmd/compile: enhance prove to deal with double-offset IsInBounds checks For chunked iterations (useful for, but not exclusive to, SIMD calculations) it is common to see the combination of ``` for ; i <= len(m)-4; i += 4 { ``` and ``` r0, r1, r2, r3 := m[i], m[i+1], m[i+2], m[i+3] `` Prove did not handle the case of len-offset1 vs index+offset2 checking, but this change fixes this. There may be other similar cases yet to handle -- this worked for the chunked loops for simd, as well as a handful in std. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: I3785df83028d517e5e5763206653b34b2befd3d0 Reviewed-on: https://go-review.googlesource.com/c/go/+/700696 Reviewed-by: Keith Randall Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI Reviewed-on: https://go-review.googlesource.com/c/go/+/708859 Reviewed-by: David Chase --- src/cmd/compile/internal/ssa/prove.go | 66 +++++++++++++++++++++++++++ test/prove.go | 12 ++--- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/cmd/compile/internal/ssa/prove.go b/src/cmd/compile/internal/ssa/prove.go index 309229b4d753b7..7b860a6f9ea2b0 100644 --- a/src/cmd/compile/internal/ssa/prove.go +++ b/src/cmd/compile/internal/ssa/prove.go @@ -2174,6 +2174,65 @@ func unsignedSubUnderflows(a, b uint64) bool { return a < b } +// checkForChunkedIndexBounds looks for index expressions of the form +// A[i+delta] where delta < K and i <= len(A)-K. That is, this is a chunked +// iteration where the index is not directly compared to the length. +func checkForChunkedIndexBounds(ft *factsTable, b *Block, index, bound *Value) bool { + if bound.Op != OpSliceLen { + return false + } + lim := ft.limits[index.ID] + if lim.min < 0 { + return false + } + i, delta := isConstDelta(index) + if i == nil { + return false + } + if delta < 0 { + return false + } + // special case for blocked iteration over a slice. + // slicelen > i + delta && <==== if clauses above + // && index >= 0 <==== if clause above + // delta >= 0 && <==== if clause above + // slicelen-K >/>= x <==== checked below + // && K >=/> delta <==== checked below + // then v > w + // example: i <=/< len - 4/3 means i+{0,1,2,3} are legal indices + for o := ft.orderings[i.ID]; o != nil; o = o.next { + if o.d != signed { + continue + } + if ow := o.w; ow.Op == OpAdd64 { + var lenOffset *Value + if ow.Args[0] == bound { + lenOffset = ow.Args[1] + } else if ow.Args[1] == bound { + lenOffset = ow.Args[0] + } + if lenOffset == nil || lenOffset.Op != OpConst64 { + continue + } + if K := -lenOffset.AuxInt; K >= 0 { + or := o.r + if or == lt { + or = lt | eq + K++ + if K < 0 { + continue + } + } + + if delta < K && or == lt|eq { + return true + } + } + } + } + return false +} + func addLocalFacts(ft *factsTable, b *Block) { // Propagate constant ranges among values in this block. // We do this before the second loop so that we have the @@ -2285,6 +2344,13 @@ func addLocalFacts(ft *factsTable, b *Block) { if v.Args[0].Op == OpSliceMake { ft.update(b, v, v.Args[0].Args[2], signed, eq) } + case OpIsInBounds: + if checkForChunkedIndexBounds(ft, b, v.Args[0], v.Args[1]) { + if b.Func.pass.debug > 0 { + b.Func.Warnl(v.Pos, "Proved %s for blocked indexing", v.Op) + } + ft.booleanTrue(v) + } case OpPhi: addLocalFactsPhi(ft, v) } diff --git a/test/prove.go b/test/prove.go index 6d2bb0962be894..bcc023dfec62fd 100644 --- a/test/prove.go +++ b/test/prove.go @@ -773,8 +773,8 @@ func indexGT0(b []byte, n int) { func unrollUpExcl(a []int) int { var i, x int for i = 0; i < len(a)-1; i += 2 { // ERROR "Induction variable: limits \[0,\?\), increment 2$" - x += a[i] // ERROR "Proved IsInBounds$" - x += a[i+1] + x += a[i] // ERROR "Proved IsInBounds$" + x += a[i+1] // ERROR "Proved IsInBounds( for blocked indexing)?$" } if i == len(a)-1 { x += a[i] @@ -786,8 +786,8 @@ func unrollUpExcl(a []int) int { func unrollUpIncl(a []int) int { var i, x int for i = 0; i <= len(a)-2; i += 2 { // ERROR "Induction variable: limits \[0,\?\], increment 2$" - x += a[i] // ERROR "Proved IsInBounds$" - x += a[i+1] + x += a[i] // ERROR "Proved IsInBounds$" + x += a[i+1] // ERROR "Proved IsInBounds( for blocked indexing)?$" } if i == len(a)-1 { x += a[i] @@ -839,7 +839,7 @@ func unrollExclStepTooLarge(a []int) int { var i, x int for i = 0; i < len(a)-1; i += 3 { x += a[i] - x += a[i+1] + x += a[i+1] // ERROR "Proved IsInBounds( for blocked indexing)?$" } if i == len(a)-1 { x += a[i] @@ -852,7 +852,7 @@ func unrollInclStepTooLarge(a []int) int { var i, x int for i = 0; i <= len(a)-2; i += 3 { x += a[i] - x += a[i+1] + x += a[i+1] // ERROR "Proved IsInBounds( for blocked indexing)?$" } if i == len(a)-1 { x += a[i] From 18cd4a1fc7d5387ae91ffc23328e4fc81f93681d Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Mon, 15 Sep 2025 21:27:19 -0400 Subject: [PATCH 51/63] cmd/compile: use the right type for spill slot Currently, when shuffling registers, if we need to spill a register, we always create a spill slot of type int64. The type doesn't actually matter, as long as it is wide enough to hold the registers. This is no longer true with SIMD registers, which could be wider than a int64. Create the slot with the proper type instead. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Test is SIMD specific so not included for now. Change-Id: I85c82e2532001bfdefe98c9446f2dd18583d49b4 Reviewed-on: https://go-review.googlesource.com/c/go/+/704055 TryBot-Bypass: Cherry Mui Reviewed-by: David Chase Reviewed-by: Junyang Shao Reviewed-on: https://go-review.googlesource.com/c/go/+/708860 LUCI-TryBot-Result: Go LUCI --- src/cmd/compile/internal/ssa/regalloc.go | 5 +---- src/cmd/compile/internal/ssa/value.go | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/cmd/compile/internal/ssa/regalloc.go b/src/cmd/compile/internal/ssa/regalloc.go index 56f9e550b2d439..26c742d7fe78ff 100644 --- a/src/cmd/compile/internal/ssa/regalloc.go +++ b/src/cmd/compile/internal/ssa/regalloc.go @@ -2690,7 +2690,6 @@ func (e *edgeState) erase(loc Location) { // findRegFor finds a register we can use to make a temp copy of type typ. func (e *edgeState) findRegFor(typ *types.Type) Location { // Which registers are possibilities. - types := &e.s.f.Config.Types m := e.s.compatRegs(typ) // Pick a register. In priority order: @@ -2724,9 +2723,7 @@ func (e *edgeState) findRegFor(typ *types.Type) Location { if !c.rematerializeable() { x := e.p.NewValue1(c.Pos, OpStoreReg, c.Type, c) // Allocate a temp location to spill a register to. - // The type of the slot is immaterial - it will not be live across - // any safepoint. Just use a type big enough to hold any register. - t := LocalSlot{N: e.s.f.NewLocal(c.Pos, types.Int64), Type: types.Int64} + t := LocalSlot{N: e.s.f.NewLocal(c.Pos, c.Type), Type: c.Type} // TODO: reuse these slots. They'll need to be erased first. e.set(t, vid, x, false, c.Pos) if e.s.f.pass.debug > regDebug { diff --git a/src/cmd/compile/internal/ssa/value.go b/src/cmd/compile/internal/ssa/value.go index 55ab23ce9a0bcd..51a70c7fd4fdcd 100644 --- a/src/cmd/compile/internal/ssa/value.go +++ b/src/cmd/compile/internal/ssa/value.go @@ -600,7 +600,7 @@ func (v *Value) removeable() bool { func AutoVar(v *Value) (*ir.Name, int64) { if loc, ok := v.Block.Func.RegAlloc[v.ID].(LocalSlot); ok { if v.Type.Size() > loc.Type.Size() { - v.Fatalf("spill/restore type %s doesn't fit in slot type %s", v.Type, loc.Type) + v.Fatalf("v%d: spill/restore type %v doesn't fit in slot type %v", v.ID, v.Type, loc.Type) } return loc.N, loc.Off } From ad3db2562edf23cb4fb9a909ea11d57b65e304fb Mon Sep 17 00:00:00 2001 From: Junyang Shao Date: Tue, 16 Sep 2025 03:27:41 +0000 Subject: [PATCH 52/63] cmd/compile: handle rematerialized op for incompatible reg constraint This CL fixes an issue raised by contributor dominikh@. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Test is SIMD specific so not included for now. Change-Id: I941b330a6ba6f6c120c69951ddd24933f2f0b3ec Reviewed-on: https://go-review.googlesource.com/c/go/+/704056 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase Reviewed-on: https://go-review.googlesource.com/c/go/+/708861 --- src/cmd/compile/internal/ssa/regalloc.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/cmd/compile/internal/ssa/regalloc.go b/src/cmd/compile/internal/ssa/regalloc.go index 26c742d7fe78ff..88861dfa14c840 100644 --- a/src/cmd/compile/internal/ssa/regalloc.go +++ b/src/cmd/compile/internal/ssa/regalloc.go @@ -2561,7 +2561,26 @@ func (e *edgeState) processDest(loc Location, vid ID, splice **Value, pos src.XP e.s.f.Fatalf("can't find source for %s->%s: %s\n", e.p, e.b, v.LongString()) } if dstReg { - x = v.copyInto(e.p) + // Handle incompatible registers. + // For #70451. + if e.s.regspec(v).outputs[0].regs®Mask(1< Date: Wed, 17 Sep 2025 14:25:16 -0400 Subject: [PATCH 53/63] cmd/compile: enhance the chunked indexing case to include reslicing this helps SIMD, but also helps plain old Go Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: Idcdacd54b6776f5c32b497bc94485052611cfa8d Reviewed-on: https://go-review.googlesource.com/c/go/+/704756 Reviewed-by: Keith Randall Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI Reviewed-on: https://go-review.googlesource.com/c/go/+/708862 Reviewed-by: David Chase --- src/cmd/compile/internal/ssa/prove.go | 34 ++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/cmd/compile/internal/ssa/prove.go b/src/cmd/compile/internal/ssa/prove.go index 7b860a6f9ea2b0..b1d49812c7d353 100644 --- a/src/cmd/compile/internal/ssa/prove.go +++ b/src/cmd/compile/internal/ssa/prove.go @@ -2177,10 +2177,18 @@ func unsignedSubUnderflows(a, b uint64) bool { // checkForChunkedIndexBounds looks for index expressions of the form // A[i+delta] where delta < K and i <= len(A)-K. That is, this is a chunked // iteration where the index is not directly compared to the length. -func checkForChunkedIndexBounds(ft *factsTable, b *Block, index, bound *Value) bool { - if bound.Op != OpSliceLen { +// if isReslice, then delta can be equal to K. +func checkForChunkedIndexBounds(ft *factsTable, b *Block, index, bound *Value, isReslice bool) bool { + if bound.Op != OpSliceLen && bound.Op != OpSliceCap { return false } + + // this is a slice bounds check against len or capacity, + // and refers back to a prior check against length, which + // will also work for the cap since that is not smaller + // than the length. + + slice := bound.Args[0] lim := ft.limits[index.ID] if lim.min < 0 { return false @@ -2206,9 +2214,9 @@ func checkForChunkedIndexBounds(ft *factsTable, b *Block, index, bound *Value) b } if ow := o.w; ow.Op == OpAdd64 { var lenOffset *Value - if ow.Args[0] == bound { + if bound := ow.Args[0]; bound.Op == OpSliceLen && bound.Args[0] == slice { lenOffset = ow.Args[1] - } else if ow.Args[1] == bound { + } else if bound := ow.Args[1]; bound.Op == OpSliceLen && bound.Args[0] == slice { lenOffset = ow.Args[0] } if lenOffset == nil || lenOffset.Op != OpConst64 { @@ -2216,12 +2224,15 @@ func checkForChunkedIndexBounds(ft *factsTable, b *Block, index, bound *Value) b } if K := -lenOffset.AuxInt; K >= 0 { or := o.r + if isReslice { + K++ + } if or == lt { or = lt | eq K++ - if K < 0 { - continue - } + } + if K < 0 { // We hate thinking about overflow + continue } if delta < K && or == lt|eq { @@ -2345,12 +2356,19 @@ func addLocalFacts(ft *factsTable, b *Block) { ft.update(b, v, v.Args[0].Args[2], signed, eq) } case OpIsInBounds: - if checkForChunkedIndexBounds(ft, b, v.Args[0], v.Args[1]) { + if checkForChunkedIndexBounds(ft, b, v.Args[0], v.Args[1], false) { if b.Func.pass.debug > 0 { b.Func.Warnl(v.Pos, "Proved %s for blocked indexing", v.Op) } ft.booleanTrue(v) } + case OpIsSliceInBounds: + if checkForChunkedIndexBounds(ft, b, v.Args[0], v.Args[1], true) { + if b.Func.pass.debug > 0 { + b.Func.Warnl(v.Pos, "Proved %s for blocked reslicing", v.Op) + } + ft.booleanTrue(v) + } case OpPhi: addLocalFactsPhi(ft, v) } From d91148c7a8b2d774ddea5c66c170d24937195df5 Mon Sep 17 00:00:00 2001 From: David Chase Date: Wed, 17 Sep 2025 17:19:15 -0400 Subject: [PATCH 54/63] cmd/compile: enhance prove to infer bounds in slice len/cap calculations the example comes up in chunked reslicing, e.g. A[i:] where i has a relationship with len(A)-K. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: Ib97dede6cfc7bbbd27b4f384988f741760686604 Reviewed-on: https://go-review.googlesource.com/c/go/+/704875 Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI Reviewed-by: Keith Randall Reviewed-on: https://go-review.googlesource.com/c/go/+/708863 Reviewed-by: David Chase --- src/cmd/compile/internal/ssa/prove.go | 65 ++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/cmd/compile/internal/ssa/prove.go b/src/cmd/compile/internal/ssa/prove.go index b1d49812c7d353..5ed5be47443d77 100644 --- a/src/cmd/compile/internal/ssa/prove.go +++ b/src/cmd/compile/internal/ssa/prove.go @@ -1766,7 +1766,8 @@ func (ft *factsTable) flowLimit(v *Value) bool { b := ft.limits[v.Args[1].ID] sub := ft.newLimit(v, a.sub(b, uint(v.Type.Size())*8)) mod := ft.detectSignedMod(v) - return sub || mod + inferred := ft.detectSliceLenRelation(v) + return sub || mod || inferred case OpNeg64, OpNeg32, OpNeg16, OpNeg8: a := ft.limits[v.Args[0].ID] bitsize := uint(v.Type.Size()) * 8 @@ -1947,6 +1948,68 @@ func (ft *factsTable) detectSignedMod(v *Value) bool { // TODO: non-powers-of-2 return false } + +// detectSliceLenRelation matches the pattern where +// 1. v := slicelen - index, OR v := slicecap - index +// AND +// 2. index <= slicelen - K +// THEN +// +// slicecap - index >= slicelen - index >= K +// +// Note that "index" is not useed for indexing in this pattern, but +// in the motivating example (chunked slice iteration) it is. +func (ft *factsTable) detectSliceLenRelation(v *Value) (inferred bool) { + if v.Op != OpSub64 { + return false + } + + if !(v.Args[0].Op == OpSliceLen || v.Args[0].Op == OpSliceCap) { + return false + } + + slice := v.Args[0].Args[0] + index := v.Args[1] + + for o := ft.orderings[index.ID]; o != nil; o = o.next { + if o.d != signed { + continue + } + or := o.r + if or != lt && or != lt|eq { + continue + } + ow := o.w + if ow.Op != OpAdd64 && ow.Op != OpSub64 { + continue + } + var lenOffset *Value + if bound := ow.Args[0]; bound.Op == OpSliceLen && bound.Args[0] == slice { + lenOffset = ow.Args[1] + } else if bound := ow.Args[1]; bound.Op == OpSliceLen && bound.Args[0] == slice { + lenOffset = ow.Args[0] + } + if lenOffset == nil || lenOffset.Op != OpConst64 { + continue + } + K := lenOffset.AuxInt + if ow.Op == OpAdd64 { + K = -K + } + if K < 0 { + continue + } + if or == lt { + K++ + } + if K < 0 { // We hate thinking about overflow + continue + } + inferred = inferred || ft.signedMin(v, K) + } + return inferred +} + func (ft *factsTable) detectSignedModByPowerOfTwo(v *Value) bool { // We're looking for: // From 003b5ce1bc15cf265e74ba1ec4eb7cf801e49986 Mon Sep 17 00:00:00 2001 From: Junyang Shao Date: Fri, 19 Sep 2025 18:38:25 +0000 Subject: [PATCH 55/63] cmd/compile: fix SIMD const rematerialization condition This CL fixes a condition for the previous fix CL 704056. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Test is SIMD specific so not included for now. Change-Id: I1f1f8c6f72870403cb3dff14755c43385dc0c933 Reviewed-on: https://go-review.googlesource.com/c/go/+/705499 LUCI-TryBot-Result: Go LUCI Reviewed-by: Cherry Mui Reviewed-on: https://go-review.googlesource.com/c/go/+/708864 Reviewed-by: David Chase --- src/cmd/compile/internal/ssa/regalloc.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/cmd/compile/internal/ssa/regalloc.go b/src/cmd/compile/internal/ssa/regalloc.go index 88861dfa14c840..e959b8ed7df2eb 100644 --- a/src/cmd/compile/internal/ssa/regalloc.go +++ b/src/cmd/compile/internal/ssa/regalloc.go @@ -2561,22 +2561,25 @@ func (e *edgeState) processDest(loc Location, vid ID, splice **Value, pos src.XP e.s.f.Fatalf("can't find source for %s->%s: %s\n", e.p, e.b, v.LongString()) } if dstReg { - // Handle incompatible registers. + // We want to rematerialize v into a register that is incompatible with v's op's register mask. + // Instead of setting the wrong register for the rematerialized v, we should find the right register + // for it and emit an additional copy to move to the desired register. // For #70451. - if e.s.regspec(v).outputs[0].regs®Mask(1< Date: Mon, 22 Sep 2025 10:57:29 -0400 Subject: [PATCH 56/63] cmd/compile: remove stores to unread parameters Currently, we remove stores to local variables that are not read. We don't do that for arguments. But arguments and locals are essentially the same. Arguments are passed by value, and are not expected to be read in the caller's frame. So we can remove the writes to them as well. One exception is the cgo_unsafe_arg directive, which makes all the arguments effectively address-taken. cgo_unsafe_arg implies ABI0, so we just skip ABI0 functions' arguments. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: I8999fc50da6a87f22c1ec23e9a0c15483b6f7df8 Reviewed-on: https://go-review.googlesource.com/c/go/+/705815 LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase Reviewed-by: Junyang Shao Reviewed-on: https://go-review.googlesource.com/c/go/+/708865 --- src/cmd/compile/internal/ssa/deadstore.go | 22 +++++++++++++++---- src/runtime/testdata/testprog/badtraceback.go | 2 ++ test/codegen/stack.go | 6 +++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/cmd/compile/internal/ssa/deadstore.go b/src/cmd/compile/internal/ssa/deadstore.go index 9e67e8339992c7..d0adff788c0a4f 100644 --- a/src/cmd/compile/internal/ssa/deadstore.go +++ b/src/cmd/compile/internal/ssa/deadstore.go @@ -7,6 +7,7 @@ package ssa import ( "cmd/compile/internal/ir" "cmd/compile/internal/types" + "cmd/internal/obj" ) // dse does dead-store elimination on the Function. @@ -213,7 +214,7 @@ func elimDeadAutosGeneric(f *Func) { case OpAddr, OpLocalAddr: // Propagate the address if it points to an auto. n, ok := v.Aux.(*ir.Name) - if !ok || n.Class != ir.PAUTO { + if !ok || (n.Class != ir.PAUTO && !isABIInternalParam(f, n)) { return } if addr[v] == nil { @@ -224,7 +225,7 @@ func elimDeadAutosGeneric(f *Func) { case OpVarDef: // v should be eliminated if we eliminate the auto. n, ok := v.Aux.(*ir.Name) - if !ok || n.Class != ir.PAUTO { + if !ok || (n.Class != ir.PAUTO && !isABIInternalParam(f, n)) { return } if elim[v] == nil { @@ -240,7 +241,7 @@ func elimDeadAutosGeneric(f *Func) { // may not be used by the inline code, but will be used by // panic processing). n, ok := v.Aux.(*ir.Name) - if !ok || n.Class != ir.PAUTO { + if !ok || (n.Class != ir.PAUTO && !isABIInternalParam(f, n)) { return } if !used.Has(n) { @@ -373,7 +374,7 @@ func elimUnreadAutos(f *Func) { if !ok { continue } - if n.Class != ir.PAUTO { + if n.Class != ir.PAUTO && !isABIInternalParam(f, n) { continue } @@ -413,3 +414,16 @@ func elimUnreadAutos(f *Func) { store.Op = OpCopy } } + +// isABIInternalParam returns whether n is a parameter of an ABIInternal +// function. For dead store elimination, we can treat parameters the same +// way as autos. Storing to a parameter can be removed if it is not read +// or address-taken. +// +// We check ABI here because for a cgo_unsafe_arg function (which is ABI0), +// all the args are effectively address-taken, but not necessarily have +// an Addr or LocalAddr op. We could probably just check for cgo_unsafe_arg, +// but ABIInternal is mostly what matters. +func isABIInternalParam(f *Func, n *ir.Name) bool { + return n.Class == ir.PPARAM && f.ABISelf.Which() == obj.ABIInternal +} diff --git a/src/runtime/testdata/testprog/badtraceback.go b/src/runtime/testdata/testprog/badtraceback.go index 09aa2b877ecf5a..455118a54371d7 100644 --- a/src/runtime/testdata/testprog/badtraceback.go +++ b/src/runtime/testdata/testprog/badtraceback.go @@ -44,6 +44,8 @@ func badLR2(arg int) { lrPtr := (*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&arg)) - lrOff)) *lrPtr = 0xbad + runtime.KeepAlive(lrPtr) // prevent dead store elimination + // Print a backtrace. This should include diagnostics for the // bad return PC and a hex dump. panic("backtrace") diff --git a/test/codegen/stack.go b/test/codegen/stack.go index 4e45d68f3816bd..59284ae88862a3 100644 --- a/test/codegen/stack.go +++ b/test/codegen/stack.go @@ -168,3 +168,9 @@ func getp1() *[4]int { func getp2() *[4]int { return nil } + +// Store to an argument without read can be removed. +func storeArg(a [2]int) { + // amd64:-`MOVQ\t\$123,.*\.a\+\d+\(SP\)` + a[1] = 123 +} From 1bca4c1673f5d90822086f34aed6de4a9bea2d93 Mon Sep 17 00:00:00 2001 From: David Chase Date: Wed, 17 Sep 2025 17:21:37 -0400 Subject: [PATCH 57/63] cmd/compile: improve slicemask removal this will be subsumed by pending changes in local slice representation, however this was easy and works well. Cherry-picked from the dev.simd branch. This CL is not necessarily SIMD specific. Apply early to reduce risk. Change-Id: I5b6eb10d257f04f906be7a8a6f2b6833992a39e8 Reviewed-on: https://go-review.googlesource.com/c/go/+/704876 Reviewed-by: Keith Randall LUCI-TryBot-Result: Go LUCI Reviewed-by: Keith Randall Reviewed-on: https://go-review.googlesource.com/c/go/+/708866 Reviewed-by: David Chase --- src/cmd/compile/internal/ssa/prove.go | 30 +++++-- test/loopbce.go | 118 +++++++++++++------------- 2 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/cmd/compile/internal/ssa/prove.go b/src/cmd/compile/internal/ssa/prove.go index 5ed5be47443d77..b4f91fd4fd12dc 100644 --- a/src/cmd/compile/internal/ssa/prove.go +++ b/src/cmd/compile/internal/ssa/prove.go @@ -2529,24 +2529,38 @@ func simplifyBlock(sdom SparseTree, ft *factsTable, b *Block) { switch v.Op { case OpSlicemask: // Replace OpSlicemask operations in b with constants where possible. - x, delta := isConstDelta(v.Args[0]) - if x == nil { + cap := v.Args[0] + x, delta := isConstDelta(cap) + if x != nil { + // slicemask(x + y) + // if x is larger than -y (y is negative), then slicemask is -1. + lim := ft.limits[x.ID] + if lim.umin > uint64(-delta) { + if cap.Op == OpAdd64 { + v.reset(OpConst64) + } else { + v.reset(OpConst32) + } + if b.Func.pass.debug > 0 { + b.Func.Warnl(v.Pos, "Proved slicemask not needed") + } + v.AuxInt = -1 + } break } - // slicemask(x + y) - // if x is larger than -y (y is negative), then slicemask is -1. - lim := ft.limits[x.ID] - if lim.umin > uint64(-delta) { - if v.Args[0].Op == OpAdd64 { + lim := ft.limits[cap.ID] + if lim.umin > 0 { + if cap.Type.Size() == 8 { v.reset(OpConst64) } else { v.reset(OpConst32) } if b.Func.pass.debug > 0 { - b.Func.Warnl(v.Pos, "Proved slicemask not needed") + b.Func.Warnl(v.Pos, "Proved slicemask not needed (by limit)") } v.AuxInt = -1 } + case OpCtz8, OpCtz16, OpCtz32, OpCtz64: // On some architectures, notably amd64, we can generate much better // code for CtzNN if we know that the argument is non-zero. diff --git a/test/loopbce.go b/test/loopbce.go index 8bc44ece9455a0..8a58d942361221 100644 --- a/test/loopbce.go +++ b/test/loopbce.go @@ -9,7 +9,7 @@ import "math" func f0a(a []int) int { x := 0 for i := range a { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - x += a[i] // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += a[i] // ERROR "Proved IsInBounds$" } return x } @@ -17,7 +17,7 @@ func f0a(a []int) int { func f0b(a []int) int { x := 0 for i := range a { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - b := a[i:] // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + b := a[i:] // ERROR "Proved IsSliceInBounds$" x += b[0] } return x @@ -26,8 +26,8 @@ func f0b(a []int) int { func f0c(a []int) int { x := 0 for i := range a { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - b := a[:i+1] // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - x += b[0] // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + b := a[:i+1] // ERROR "Proved IsSliceInBounds$" + x += b[0] // ERROR "Proved IsInBounds$" } return x } @@ -43,7 +43,7 @@ func f1(a []int) int { func f2(a []int) int { x := 0 for i := 1; i < len(a); i++ { // ERROR "Induction variable: limits \[1,\?\), increment 1$" - x += a[i] // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += a[i] // ERROR "Proved IsInBounds$" } return x } @@ -51,7 +51,7 @@ func f2(a []int) int { func f4(a [10]int) int { x := 0 for i := 0; i < len(a); i += 2 { // ERROR "Induction variable: limits \[0,8\], increment 2$" - x += a[i] // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += a[i] // ERROR "Proved IsInBounds$" } return x } @@ -91,7 +91,7 @@ func f5_int8(a [10]int) int { //go:noinline func f6(a []int) { for i := range a { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - b := a[0:i] // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + b := a[0:i] // ERROR "Proved IsSliceInBounds$" f6(b) } } @@ -99,7 +99,7 @@ func f6(a []int) { func g0a(a string) int { x := 0 for i := 0; i < len(a); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - x += int(a[i]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i]) // ERROR "Proved IsInBounds$" } return x } @@ -107,7 +107,7 @@ func g0a(a string) int { func g0b(a string) int { x := 0 for i := 0; len(a) > i; i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - x += int(a[i]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i]) // ERROR "Proved IsInBounds$" } return x } @@ -115,7 +115,7 @@ func g0b(a string) int { func g0c(a string) int { x := 0 for i := len(a); i > 0; i-- { // ERROR "Induction variable: limits \(0,\?\], increment 1$" - x += int(a[i-1]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i-1]) // ERROR "Proved IsInBounds$" } return x } @@ -123,7 +123,7 @@ func g0c(a string) int { func g0d(a string) int { x := 0 for i := len(a); 0 < i; i-- { // ERROR "Induction variable: limits \(0,\?\], increment 1$" - x += int(a[i-1]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i-1]) // ERROR "Proved IsInBounds$" } return x } @@ -131,7 +131,7 @@ func g0d(a string) int { func g0e(a string) int { x := 0 for i := len(a) - 1; i >= 0; i-- { // ERROR "Induction variable: limits \[0,\?\], increment 1$" - x += int(a[i]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i]) // ERROR "Proved IsInBounds$" } return x } @@ -139,7 +139,7 @@ func g0e(a string) int { func g0f(a string) int { x := 0 for i := len(a) - 1; 0 <= i; i-- { // ERROR "Induction variable: limits \[0,\?\], increment 1$" - x += int(a[i]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i]) // ERROR "Proved IsInBounds$" } return x } @@ -148,7 +148,7 @@ func g1() int { a := "evenlength" x := 0 for i := 0; i < len(a); i += 2 { // ERROR "Induction variable: limits \[0,8\], increment 2$" - x += int(a[i]) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + x += int(a[i]) // ERROR "Proved IsInBounds$" } return x } @@ -158,7 +158,7 @@ func g2() int { x := 0 for i := 0; i < len(a); i += 2 { // ERROR "Induction variable: limits \[0,8\], increment 2$" j := i - if a[i] == 'e' { // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + if a[i] == 'e' { // ERROR "Proved IsInBounds$" j = j + 1 } x += int(a[j]) @@ -169,29 +169,29 @@ func g2() int { func g3a() { a := "this string has length 25" for i := 0; i < len(a); i += 5 { // ERROR "Induction variable: limits \[0,20\], increment 5$" - useString(a[i:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useString(a[:i+3]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useString(a[:i+5]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useString(a[i:]) // ERROR "Proved IsSliceInBounds$" "Proved slicemask not needed \(by limit\)$" + useString(a[:i+3]) // ERROR "Proved IsSliceInBounds$" + useString(a[:i+5]) // ERROR "Proved IsSliceInBounds$" useString(a[:i+6]) } } func g3b(a string) { for i := 0; i < len(a); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - useString(a[i+1:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useString(a[i+1:]) // ERROR "Proved IsSliceInBounds$" } } func g3c(a string) { for i := 0; i < len(a); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - useString(a[:i+1]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useString(a[:i+1]) // ERROR "Proved IsSliceInBounds$" } } func h1(a []byte) { c := a[:128] for i := range c { // ERROR "Induction variable: limits \[0,128\), increment 1$" - c[i] = byte(i) // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + c[i] = byte(i) // ERROR "Proved IsInBounds$" } } @@ -208,11 +208,11 @@ func k0(a [100]int) [100]int { continue } a[i-11] = i - a[i-10] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" - a[i-5] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" - a[i] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" - a[i+5] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" - a[i+10] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + a[i-10] = i // ERROR "Proved IsInBounds$" + a[i-5] = i // ERROR "Proved IsInBounds$" + a[i] = i // ERROR "Proved IsInBounds$" + a[i+5] = i // ERROR "Proved IsInBounds$" + a[i+10] = i // ERROR "Proved IsInBounds$" a[i+11] = i } return a @@ -225,12 +225,12 @@ func k1(a [100]int) [100]int { continue } useSlice(a[:i-11]) - useSlice(a[:i-10]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[:i-5]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[:i]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[:i+5]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[:i+10]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[:i+11]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useSlice(a[:i-10]) // ERROR "Proved IsSliceInBounds$" + useSlice(a[:i-5]) // ERROR "Proved IsSliceInBounds$" + useSlice(a[:i]) // ERROR "Proved IsSliceInBounds$" + useSlice(a[:i+5]) // ERROR "Proved IsSliceInBounds$" + useSlice(a[:i+10]) // ERROR "Proved IsSliceInBounds$" + useSlice(a[:i+11]) // ERROR "Proved IsSliceInBounds$" useSlice(a[:i+12]) } @@ -243,13 +243,13 @@ func k2(a [100]int) [100]int { // This is a trick to prohibit sccp to optimize out the following out of bound check continue } - useSlice(a[i-11:]) - useSlice(a[i-10:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[i-5:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[i:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[i+5:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[i+10:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" - useSlice(a[i+11:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useSlice(a[i-11:]) // ERROR "Proved slicemask not needed \(by limit\)$" + useSlice(a[i-10:]) // ERROR "Proved IsSliceInBounds$" "Proved slicemask not needed \(by limit\)$" + useSlice(a[i-5:]) // ERROR "Proved IsSliceInBounds$" "Proved slicemask not needed \(by limit\)$" + useSlice(a[i:]) // ERROR "Proved IsSliceInBounds$" "Proved slicemask not needed \(by limit\)$" + useSlice(a[i+5:]) // ERROR "Proved IsSliceInBounds$" "Proved slicemask not needed \(by limit\)$" + useSlice(a[i+10:]) // ERROR "Proved IsSliceInBounds$" "Proved slicemask not needed \(by limit\)$" + useSlice(a[i+11:]) // ERROR "Proved IsSliceInBounds$" useSlice(a[i+12:]) } return a @@ -262,7 +262,7 @@ func k3(a [100]int) [100]int { continue } a[i+9] = i - a[i+10] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + a[i+10] = i // ERROR "Proved IsInBounds$" a[i+11] = i } return a @@ -275,7 +275,7 @@ func k3neg(a [100]int) [100]int { continue } a[i+9] = i - a[i+10] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + a[i+10] = i // ERROR "Proved IsInBounds$" a[i+11] = i } return a @@ -288,7 +288,7 @@ func k3neg2(a [100]int) [100]int { continue } a[i+9] = i - a[i+10] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + a[i+10] = i // ERROR "Proved IsInBounds$" a[i+11] = i } return a @@ -299,7 +299,7 @@ func k4(a [100]int) [100]int { // and it isn't worth adding that special case to prove. min := (-1)<<63 + 1 for i := min; i < min+50; i++ { // ERROR "Induction variable: limits \[-9223372036854775807,-9223372036854775757\), increment 1$" - a[i-min] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + a[i-min] = i // ERROR "Proved IsInBounds$" } return a } @@ -307,8 +307,8 @@ func k4(a [100]int) [100]int { func k5(a [100]int) [100]int { max := (1 << 63) - 1 for i := max - 50; i < max; i++ { // ERROR "Induction variable: limits \[9223372036854775757,9223372036854775807\), increment 1$" - a[i-max+50] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" - a[i-(max-70)] = i // ERROR "(\([0-9]+\) )?Proved IsInBounds$" + a[i-max+50] = i // ERROR "Proved IsInBounds$" + a[i-(max-70)] = i // ERROR "Proved IsInBounds$" } return a } @@ -374,22 +374,22 @@ func d4() { } func d5() { - for i := int64(math.MinInt64 + 9); i > math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775803,-9223372036854775799\], increment 4" + for i := int64(math.MinInt64 + 9); i > math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775803,-9223372036854775799\], increment 4$" useString("foo") } - for i := int64(math.MinInt64 + 8); i > math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775804,-9223372036854775800\], increment 4" + for i := int64(math.MinInt64 + 8); i > math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775804,-9223372036854775800\], increment 4$" useString("foo") } for i := int64(math.MinInt64 + 7); i > math.MinInt64+2; i -= 4 { useString("foo") } - for i := int64(math.MinInt64 + 6); i > math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775802,-9223372036854775802\], increment 4" + for i := int64(math.MinInt64 + 6); i > math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775802,-9223372036854775802\], increment 4$" useString("foo") } - for i := int64(math.MinInt64 + 9); i >= math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775803,-9223372036854775799\], increment 4" + for i := int64(math.MinInt64 + 9); i >= math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775803,-9223372036854775799\], increment 4$" useString("foo") } - for i := int64(math.MinInt64 + 8); i >= math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775804,-9223372036854775800\], increment 4" + for i := int64(math.MinInt64 + 8); i >= math.MinInt64+2; i -= 4 { // ERROR "Induction variable: limits \[-9223372036854775804,-9223372036854775800\], increment 4$" useString("foo") } for i := int64(math.MinInt64 + 7); i >= math.MinInt64+2; i -= 4 { @@ -410,23 +410,23 @@ func bce1() { panic("invalid test: modulos should differ") } - for i := b; i < a; i += z { // ERROR "Induction variable: limits \[-1547,9223372036854772720\], increment 1337" + for i := b; i < a; i += z { // ERROR "Induction variable: limits \[-1547,9223372036854772720\], increment 1337$" useString("foobar") } } func nobce2(a string) { for i := int64(0); i < int64(len(a)); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - useString(a[i:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useString(a[i:]) // ERROR "Proved IsSliceInBounds$" } for i := int64(0); i < int64(len(a))-31337; i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" - useString(a[i:]) // ERROR "(\([0-9]+\) )?Proved IsSliceInBounds$" + useString(a[i:]) // ERROR "Proved IsSliceInBounds$" } - for i := int64(0); i < int64(len(a))+int64(-1<<63); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" "Disproved Less64" + for i := int64(0); i < int64(len(a))+int64(-1<<63); i++ { // ERROR "Disproved Less64$" "Induction variable: limits \[0,\?\), increment 1$" useString(a[i:]) } j := int64(len(a)) - 123 - for i := int64(0); i < j+123+int64(-1<<63); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" "Disproved Less64" + for i := int64(0); i < j+123+int64(-1<<63); i++ { // ERROR "Disproved Less64$" "Induction variable: limits \[0,\?\), increment 1$" useString(a[i:]) } for i := int64(0); i < j+122+int64(-1<<63); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$" @@ -455,16 +455,16 @@ func issue26116a(a []int) { func stride1(x *[7]int) int { s := 0 - for i := 0; i <= 8; i += 3 { // ERROR "Induction variable: limits \[0,6\], increment 3" - s += x[i] // ERROR "Proved IsInBounds" + for i := 0; i <= 8; i += 3 { // ERROR "Induction variable: limits \[0,6\], increment 3$" + s += x[i] // ERROR "Proved IsInBounds$" } return s } func stride2(x *[7]int) int { s := 0 - for i := 0; i < 9; i += 3 { // ERROR "Induction variable: limits \[0,6\], increment 3" - s += x[i] // ERROR "Proved IsInBounds" + for i := 0; i < 9; i += 3 { // ERROR "Induction variable: limits \[0,6\], increment 3$" + s += x[i] // ERROR "Proved IsInBounds$" } return s } From ee5369b003b17b34aa6417cf8c9b702f1cd76da1 Mon Sep 17 00:00:00 2001 From: qmuntal Date: Fri, 3 Oct 2025 11:18:47 +0200 Subject: [PATCH 58/63] cmd/link: add LIBRARY statement only with -buildmode=cshared When creating a .def file for Windows linking, add a LIBRARY statement only when building a DLL with -buildmode=cshared. That statement is documented to instruct the linker to create a DLL, overriding any other flag that might indicate building an executable. Fixes #75734 Change-Id: I0231435df70b71a493a39deb639f6328a8e354f6 Reviewed-on: https://go-review.googlesource.com/c/go/+/708815 Reviewed-by: Carlos Amedee Reviewed-by: Dominic Della Valle Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI --- src/cmd/link/internal/ld/pe.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cmd/link/internal/ld/pe.go b/src/cmd/link/internal/ld/pe.go index 0f0650e5e149e3..e0186f46b035d9 100644 --- a/src/cmd/link/internal/ld/pe.go +++ b/src/cmd/link/internal/ld/pe.go @@ -1758,7 +1758,9 @@ func peCreateExportFile(ctxt *Link, libName string) (fname string) { fname = filepath.Join(*flagTmpdir, "export_file.def") var buf bytes.Buffer - fmt.Fprintf(&buf, "LIBRARY %s\n", libName) + if ctxt.BuildMode == BuildModeCShared { + fmt.Fprintf(&buf, "LIBRARY %s\n", libName) + } buf.WriteString("EXPORTS\n") ldr := ctxt.loader From 2a71af11fc7e45903be9ab84e59c668bb051f528 Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Wed, 24 Sep 2025 16:40:55 +0200 Subject: [PATCH 59/63] net/url: improve URL docs The Raw fields are confusing and easy to use by mistake. Adds more context in comments to these fields. Also the current docs (and the names of these fields) of these boolean fields are not obvious that parser might produce them, so clarify that Change-Id: I6a6a69644834c3ccbf657147f771930b6875f721 Reviewed-on: https://go-review.googlesource.com/c/go/+/706515 Reviewed-by: Florian Lehner Reviewed-by: Sean Liao Reviewed-by: Damien Neil Reviewed-by: Carlos Amedee Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI --- src/net/url/url.go | 50 ++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/net/url/url.go b/src/net/url/url.go index 2a57659460373d..015c5b2751974a 100644 --- a/src/net/url/url.go +++ b/src/net/url/url.go @@ -364,25 +364,41 @@ func escape(s string, mode encoding) string { // A consequence is that it is impossible to tell which slashes in the Path were // slashes in the raw URL and which were %2f. This distinction is rarely important, // but when it is, the code should use the [URL.EscapedPath] method, which preserves -// the original encoding of Path. +// the original encoding of Path. The Fragment field is also stored in decoded form, +// use [URL.EscapedFragment] to retrieve the original encoding. // -// The RawPath field is an optional field which is only set when the default -// encoding of Path is different from the escaped path. See the EscapedPath method -// for more details. -// -// URL's String method uses the EscapedPath method to obtain the path. +// The [URL.String] method uses the [URL.EscapedPath] method to obtain the path. type URL struct { - Scheme string - Opaque string // encoded opaque data - User *Userinfo // username and password information - Host string // host or host:port (see Hostname and Port methods) - Path string // path (relative paths may omit leading slash) - RawPath string // encoded path hint (see EscapedPath method) - OmitHost bool // do not emit empty host (authority) - ForceQuery bool // append a query ('?') even if RawQuery is empty - RawQuery string // encoded query values, without '?' - Fragment string // fragment for references, without '#' - RawFragment string // encoded fragment hint (see EscapedFragment method) + Scheme string + Opaque string // encoded opaque data + User *Userinfo // username and password information + Host string // "host" or "host:port" (see Hostname and Port methods) + Path string // path (relative paths may omit leading slash) + Fragment string // fragment for references (without '#') + + // RawQuery contains the encoded query values, without the initial '?'. + // Use URL.Query to decode the query. + RawQuery string + + // RawPath is an optional field containing an encoded path hint. + // See the EscapedPath method for more details. + // + // In general, code should call EscapedPath instead of reading RawPath. + RawPath string + + // RawFragment is an optional field containing an encoded fragment hint. + // See the EscapedFragment method for more details. + // + // In general, code should call EscapedFragment instead of reading RawFragment. + RawFragment string + + // ForceQuery indicates whether the original URL contained a query ('?') character. + // When set, the String method will include a trailing '?', even when RawQuery is empty. + ForceQuery bool + + // OmitHost indicates the URL has an empty host (authority). + // When set, the String method will not include the host when it is empty. + OmitHost bool } // User returns a [Userinfo] containing the provided username From 51fd07f31737a9aa162f18a1ec3a46ea80ba2f29 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sat, 4 Oct 2025 23:07:01 +0400 Subject: [PATCH 60/63] add errors Match and IsAny Add Match and IsAny, but functions are slow. --- src/errors/example_test.go | 54 +++++++++ src/errors/wrap.go | 117 ++++++++++++++++++++ src/errors/wrap_test.go | 219 +++++++++++++++++++++++++++++++++++++ 3 files changed, 390 insertions(+) diff --git a/src/errors/example_test.go b/src/errors/example_test.go index 92ef36b1010edb..fe20214bb4294d 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -123,3 +123,57 @@ func ExampleUnwrap() { // error2: [error1] // error1 } + +func ExampleIsAny() { + var ( + ErrNotFound = errors.New("not found") + ErrPermission = errors.New("permission denied") + ErrTimeout = errors.New("timeout") + ) + + // Simulate receiving an error + err := fmt.Errorf("database query failed: %w", ErrTimeout) + + // Check if the error matches any of the known errors + if errors.IsAny(err, ErrNotFound, ErrPermission, ErrTimeout) { + fmt.Println("error is one of the expected types") + } + + // Check against a different set + if !errors.IsAny(err, ErrNotFound, ErrPermission) { + fmt.Println("error is not a not-found or permission error") + } + + // Output: + // error is one of the expected types + // error is not a not-found or permission error +} + +func ExampleMatch() { + var ( + ErrNotFound = errors.New("not found") + ErrNetworkIssue = errors.New("network issue") + ErrDiskFull = errors.New("disk full") + ) + + err := fmt.Errorf("operation failed: %w", ErrNetworkIssue) + + // Match returns the matched error from the targets + if matched := errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull); matched != nil { + fmt.Println("matched error:", matched) + } + + // Can be used in a switch statement + switch errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull) { + case ErrNotFound: + fmt.Println("resource not found") + case ErrNetworkIssue: + fmt.Println("network issue detected") + case ErrDiskFull: + fmt.Println("disk is full") + } + + // Output: + // matched error: network issue + // network issue detected +} diff --git a/src/errors/wrap.go b/src/errors/wrap.go index 2ebb951f1de93d..04c37ce1f77861 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -206,3 +206,120 @@ func asType[E error](err error, ppe **E) (_ E, _ bool) { } } } + +func IsAnySlow(err error, targets ...error) bool { + if err == nil { + for _, target := range targets { + if target == nil { + return true + } + } + return false + } + if len(targets) == 0 { + return false + } + + for _, target := range targets { + if Is(err, target) { + return true + } + } + return false +} + +// IsAny reports whether any error in err's tree matches any of the target errors. +// +// The tree consists of err itself, followed by the errors obtained by repeatedly +// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple +// errors, IsAny examines err followed by a depth-first traversal of its children. +// +// IsAny returns true if [Is](err, target) returns true for any target in targets. +func IsAny(err error, targets ...error) bool { + _, found := match(err, targets) + + return found +} + +// Match returns the matched error in targets for err. +// +// The tree consists of err itself, followed by the errors obtained by repeatedly +// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple +// errors, Match examines err followed by a depth-first traversal of its children. +// +// Match returns the first target error for which [Is](err, target) returns true. +// If no target matches, Match returns nil. +func Match(err error, targets ...error) error { + matched, _ := match(err, targets) + + return matched +} + +func match(err error, targets []error) (error, bool) { + if err == nil { + for _, target := range targets { + if target == nil { + return nil, true + } + } + return nil, false + } + + if len(targets) == 0 { + return nil, false + } else if len(targets) == 1 { + if Is(err, targets[0]) { + return targets[0], true + } + + return nil, false + } + + targetMap := make(map[error]struct{}, len(targets)) + for _, target := range targets { + if target != nil && reflectlite.TypeOf(target).Comparable() { + targetMap[target] = struct{}{} + } + } + + isErrComparable := reflectlite.TypeOf(err).Comparable() + for { + if isErrComparable && len(targetMap) > 0 { + if _, ok := targetMap[err]; ok { + for _, target := range targets { + if target == err { + return target, true + } + } + } + } + + if x, ok := err.(interface{ Is(error) bool }); ok { + for _, target := range targets { + if target != nil && x.Is(target) { + return target, true + } + } + } + + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return nil, false + } + isErrComparable = reflectlite.TypeOf(err).Comparable() + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if err != nil { + if matched, found := match(err, targets); matched != nil { + return matched, found + } + } + } + return nil, false + default: + return nil, false + } + } +} diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go index 81c795a6bb8b18..3a505efcdf9f41 100644 --- a/src/errors/wrap_test.go +++ b/src/errors/wrap_test.go @@ -436,3 +436,222 @@ func (errorUncomparable) Is(target error) bool { _, ok := target.(errorUncomparable) return ok } + +func TestIsAny(t *testing.T) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + erra := wrapped{"wrap a", err1} + errb := wrapped{"wrap b", err2} + + poser := &poser{"either 1 or 3", func(err error) bool { + return err == err1 || err == err3 + }} + + testCases := []struct { + err error + targets []error + match bool + }{ + // Basic cases + {nil, []error{nil}, true}, + {nil, []error{err1}, false}, + {err1, []error{nil}, false}, + {err1, []error{err1}, true}, + {err1, []error{err2}, false}, + {err1, []error{err1, err2}, true}, + {err1, []error{err2, err1}, true}, + {err1, []error{err2, err3}, false}, + + // Wrapped errors + {erra, []error{err1}, true}, + {erra, []error{err2}, false}, + {erra, []error{err1, err2}, true}, + {erra, []error{err2, err1}, true}, + {erra, []error{err2, err3}, false}, + + // Multiple targets with wrapped errors + {errb, []error{err1, err2, err3}, true}, + {errb, []error{err1, err3}, false}, + + // Posers + {poser, []error{err1}, true}, + {poser, []error{err3}, true}, + {poser, []error{err2}, false}, + {poser, []error{err1, err2}, true}, + {poser, []error{err2, err3}, true}, + {poser, []error{err2, erra}, false}, + + // Multi errors + {multiErr{}, []error{err1}, false}, + {multiErr{err1, err2}, []error{err1}, true}, + {multiErr{err1, err2}, []error{err2}, true}, + {multiErr{err1, err2}, []error{err3}, false}, + {multiErr{err1, err2}, []error{err3, err1}, true}, + {multiErr{err1, err2}, []error{err3, erra}, false}, + {multiErr{erra, errb}, []error{err1, err2}, true}, + {multiErr{erra, errb}, []error{err3, err1}, true}, + + // Empty targets + {err1, []error{}, false}, + {nil, []error{}, false}, + + // Uncomparable errors + {errorUncomparable{}, []error{errorUncomparable{}}, true}, + {&errorUncomparable{}, []error{errorUncomparable{}}, true}, + {errorUncomparable{}, []error{err1, errorUncomparable{}}, true}, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { + if got := errors.IsAny(tc.err, tc.targets...); got != tc.match { + t.Errorf("IsAny(%v, %v) = %v, want %v", tc.err, tc.targets, got, tc.match) + } + }) + } +} + +func TestMatch(t *testing.T) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + erra := wrapped{"wrap a", err1} + + poser := &poser{"either 1 or 3", func(err error) bool { + return err == err1 || err == err3 + }} + + testCases := []struct { + err error + targets []error + want error // the expected matched error + }{ + {err1, []error{err1}, err1}, + {err1, []error{err2}, nil}, + {err1, []error{err1, err2}, err1}, + {err1, []error{err2, err1}, err1}, // Returns first match (err1) + {err1, []error{err2, err3}, nil}, + {erra, []error{err1, err2}, err1}, + {erra, []error{err2, err1}, err1}, // erra wraps err1, so matches err1 + {erra, []error{err2, err3}, nil}, + {nil, []error{nil}, nil}, + {nil, []error{err1}, nil}, + {err1, []error{}, nil}, + + // Posers - note that the poser matches err1 or err3 + {poser, []error{err1}, err1}, + {poser, []error{err3}, err3}, + {poser, []error{err2}, nil}, + {poser, []error{err2, err1}, err1}, + {poser, []error{err1, err3}, err1}, // Returns first match + + // Multi errors + {multiErr{err1, err2}, []error{err1}, err1}, + {multiErr{err1, err2}, []error{err2}, err2}, + {multiErr{err1, err2}, []error{err3}, nil}, + {multiErr{err1, err2}, []error{err3, err2}, err2}, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("case_%d", i), func(t *testing.T) { + got := errors.Match(tc.err, tc.targets...) + if got != tc.want { + t.Errorf("Match(%v, %v) = %v, want %v", tc.err, tc.targets, got, tc.want) + } + }) + } +} + +// TODO remove +func BenchmarkIsAnySlow(b *testing.B) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} + + b.Run("one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAnySlow(err, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAnySlow(err, err2, err3, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.IsAnySlow(err, err2, err3) { + b.Fatal("IsAny should not match") + } + } + }) +} + +func BenchmarkIsAny(b *testing.B) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} + + b.Run("one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAny(err, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !errors.IsAny(err, err2, err3, err1) { + b.Fatal("IsAny failed") + } + } + }) + + b.Run("no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.IsAny(err, err2, err3) { + b.Fatal("IsAny should not match") + } + } + }) +} + +func BenchmarkMatch(b *testing.B) { + err1 := errors.New("1") + err2 := errors.New("2") + err3 := errors.New("3") + err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} + + b.Run("one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.Match(err, err1) != err1 { + b.Fatal("Match failed") + } + } + }) + + b.Run("three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.Match(err, err2, err3, err1) != err1 { + b.Fatal("Match failed") + } + } + }) + + b.Run("no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if errors.Match(err, err2, err3) != nil { + b.Fatal("Match should not match") + } + } + }) +} From 0fa8635c1eef1ef53ce61960109ed93f84081814 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sat, 4 Oct 2025 23:33:46 +0400 Subject: [PATCH 61/63] add errors Match and IsAny improved performance of Match and IsAny --- src/errors/wrap.go | 33 +++------------- src/errors/wrap_test.go | 87 +++++++++++++++++++---------------------- 2 files changed, 47 insertions(+), 73 deletions(-) diff --git a/src/errors/wrap.go b/src/errors/wrap.go index 04c37ce1f77861..ba1dd9f50253c7 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -207,27 +207,6 @@ func asType[E error](err error, ppe **E) (_ E, _ bool) { } } -func IsAnySlow(err error, targets ...error) bool { - if err == nil { - for _, target := range targets { - if target == nil { - return true - } - } - return false - } - if len(targets) == 0 { - return false - } - - for _, target := range targets { - if Is(err, target) { - return true - } - } - return false -} - // IsAny reports whether any error in err's tree matches any of the target errors. // // The tree consists of err itself, followed by the errors obtained by repeatedly @@ -282,15 +261,15 @@ func match(err error, targets []error) (error, bool) { } } + return matching(err, targets, targetMap) +} + +func matching(err error, targets []error, targetMap map[error]struct{}) (error, bool) { isErrComparable := reflectlite.TypeOf(err).Comparable() for { if isErrComparable && len(targetMap) > 0 { if _, ok := targetMap[err]; ok { - for _, target := range targets { - if target == err { - return target, true - } - } + return err, true } } @@ -312,7 +291,7 @@ func match(err error, targets []error) (error, bool) { case interface{ Unwrap() []error }: for _, err := range x.Unwrap() { if err != nil { - if matched, found := match(err, targets); matched != nil { + if matched, found := matching(err, targets, targetMap); matched != nil { return matched, found } } diff --git a/src/errors/wrap_test.go b/src/errors/wrap_test.go index 3a505efcdf9f41..22da8f0b3ca34e 100644 --- a/src/errors/wrap_test.go +++ b/src/errors/wrap_test.go @@ -562,36 +562,15 @@ func TestMatch(t *testing.T) { } } -// TODO remove -func BenchmarkIsAnySlow(b *testing.B) { - err1 := errors.New("1") - err2 := errors.New("2") - err3 := errors.New("3") - err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} - - b.Run("one_target", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAnySlow(err, err1) { - b.Fatal("IsAny failed") - } +// isAnySlow is a naive implementation of IsAny for benchmarking purposes. +func isAnySlow(err error, targets ...error) bool { + for _, target := range targets { + if errors.Is(err, target) { + return true } - }) - - b.Run("three_targets", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAnySlow(err, err2, err3, err1) { - b.Fatal("IsAny failed") - } - } - }) + } - b.Run("no_match", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if errors.IsAnySlow(err, err2, err3) { - b.Fatal("IsAny should not match") - } - } - }) + return false } func BenchmarkIsAny(b *testing.B) { @@ -600,29 +579,45 @@ func BenchmarkIsAny(b *testing.B) { err3 := errors.New("3") err := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}} - b.Run("one_target", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAny(err, err1) { - b.Fatal("IsAny failed") + testCases := []struct { + name string + fn func(error, ...error) bool + }{ + { + name: "IsAny", + fn: errors.IsAny, + }, + { + name: "isAnySlow", + fn: isAnySlow, + }, + } + + for _, tc := range testCases { + b.Run(tc.name+"_one_target", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !tc.fn(err, err1) { + b.Fatal(tc.name, "failed") + } } - } - }) + }) - b.Run("three_targets", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if !errors.IsAny(err, err2, err3, err1) { - b.Fatal("IsAny failed") + b.Run(tc.name+"three_targets", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if !tc.fn(err, err2, err3, err1) { + b.Fatal(tc.name, "failed") + } } - } - }) + }) - b.Run("no_match", func(b *testing.B) { - for i := 0; i < b.N; i++ { - if errors.IsAny(err, err2, err3) { - b.Fatal("IsAny should not match") + b.Run(tc.name+"no_match", func(b *testing.B) { + for i := 0; i < b.N; i++ { + if tc.fn(err, err2, err3) { + b.Fatal(tc.name, "should not match") + } } - } - }) + }) + } } func BenchmarkMatch(b *testing.B) { From b3d0973da1d98dd7e3d8226d9b62577806cad734 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sat, 4 Oct 2025 23:44:28 +0400 Subject: [PATCH 62/63] add errors Match and IsAny Simplified documentation --- src/errors/wrap.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/errors/wrap.go b/src/errors/wrap.go index ba1dd9f50253c7..87e82600b4873e 100644 --- a/src/errors/wrap.go +++ b/src/errors/wrap.go @@ -212,22 +212,21 @@ func asType[E error](err error, ppe **E) (_ E, _ bool) { // The tree consists of err itself, followed by the errors obtained by repeatedly // calling its Unwrap() error or Unwrap() []error method. When err wraps multiple // errors, IsAny examines err followed by a depth-first traversal of its children. -// -// IsAny returns true if [Is](err, target) returns true for any target in targets. func IsAny(err error, targets ...error) bool { _, found := match(err, targets) return found } -// Match returns the matched error in targets for err. +// Match returns the first target error from targets that matches any error in err's tree. // // The tree consists of err itself, followed by the errors obtained by repeatedly // calling its Unwrap() error or Unwrap() []error method. When err wraps multiple // errors, Match examines err followed by a depth-first traversal of its children. // -// Match returns the first target error for which [Is](err, target) returns true. -// If no target matches, Match returns nil. +// Match returns the first target from targets if an err is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +// If no target matches the err, Match returns nil. func Match(err error, targets ...error) error { matched, _ := match(err, targets) From 36dafdbbd04d5c8757d14cb737ee9e9759505169 Mon Sep 17 00:00:00 2001 From: "ilia.sergunin" Date: Sun, 5 Oct 2025 00:00:17 +0400 Subject: [PATCH 63/63] add errors Match and IsAny Simplified examples --- src/errors/example_test.go | 59 ++++++++++++-------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/errors/example_test.go b/src/errors/example_test.go index fe20214bb4294d..77193a05b95bcf 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -125,55 +125,34 @@ func ExampleUnwrap() { } func ExampleIsAny() { - var ( - ErrNotFound = errors.New("not found") - ErrPermission = errors.New("permission denied") - ErrTimeout = errors.New("timeout") - ) - - // Simulate receiving an error - err := fmt.Errorf("database query failed: %w", ErrTimeout) - - // Check if the error matches any of the known errors - if errors.IsAny(err, ErrNotFound, ErrPermission, ErrTimeout) { - fmt.Println("error is one of the expected types") - } - - // Check against a different set - if !errors.IsAny(err, ErrNotFound, ErrPermission) { - fmt.Println("error is not a not-found or permission error") + if _, err := os.Open("non-existing"); err != nil { + if errors.IsAny(err, fs.ErrNotExist, fs.ErrInvalid) { + fmt.Println("file does not exist") + } else { + fmt.Println(err) + } } - // Output: - // error is one of the expected types - // error is not a not-found or permission error + // file does not exist } func ExampleMatch() { - var ( - ErrNotFound = errors.New("not found") - ErrNetworkIssue = errors.New("network issue") - ErrDiskFull = errors.New("disk full") - ) + _, err := os.Open("non-existing") - err := fmt.Errorf("operation failed: %w", ErrNetworkIssue) - - // Match returns the matched error from the targets - if matched := errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull); matched != nil { + matched := errors.Match(err, fs.ErrNotExist, fs.ErrInvalid) + if matched != nil { fmt.Println("matched error:", matched) + } else { + fmt.Println("no match") } - // Can be used in a switch statement - switch errors.Match(err, ErrNotFound, ErrNetworkIssue, ErrDiskFull) { - case ErrNotFound: - fmt.Println("resource not found") - case ErrNetworkIssue: - fmt.Println("network issue detected") - case ErrDiskFull: - fmt.Println("disk is full") + switch matched { + case fs.ErrNotExist: + fmt.Println("file does not exist") + case fs.ErrInvalid: + fmt.Println("invalid argument") } - // Output: - // matched error: network issue - // network issue detected + // matched error: file does not exist + // file does not exist }