Repository: go-delve/delve

Issue:
Debugger hangs when debugging with breakpoints
When a test case with breakpoints in code or test case the itself, is executed, below error is thrown in logs and the test case execution hangs forever.

```
org.jetbrains.concurrency.MessageError: Internal debugger error: <nil> not a String
runtime.gopanic (0x43cdf1)
	/mnt/agent/temp/buildTmp/go/src/runtime/panic.go:770
go/constant.StringVal (0x6173e4)
	/mnt/agent/temp/buildTmp/go/src/go/constant/value.go:500
github.com/go-delve/delve/pkg/proc.(*G).Labels (0x8436f9)
	/mnt/agent/work/4c3e5e3eabfaf2d2/pkg/proc/variables.go:594
github.com/go-delve/delve/service/api.ConvertGoroutine (0x8650d8)
	/mnt/agent/work/4c3e5e3eabfaf2d2/service/api/conversions.go:309
github.com/go-delve/delve/service/debugger.(*Debugger).state (0x8fc0b7)
	/mnt/agent/work/4c3e5e3eabfaf2d2/service/debugger/debugger.go:586
github.com/go-delve/delve/service/debugger.(*Debugger).Command (0x9017cc)
	/mnt/agent/work/4c3e5e3eabfaf2d2/service/debugger/debugger.go:1219
github.com/go-delve/delve/service/rpc2.(*RPCServer).Command (0xa4db1c)
	/mnt/agent/work/4c3e5e3eabfaf2d2/service/rpc2/server.go:130
reflect.Value.call (0x4e3f45)
	/mnt/agent/temp/buildTmp/go/src/reflect/value.go:596
reflect.Value.Call (0x4e3058)
	/mnt/agent/temp/buildTmp/go/src/reflect/value.go:380
github.com/go-delve/delve/service/rpccommon.(*ServerImpl).serveJSONCodec.func3 (0xb3d314)
	/mnt/agent/work/4c3e5e3eabfaf2d2/service/rpccommon/server.go:369
runtime.goexit (0x4736c0)
	/mnt/agent/temp/buildTmp/go/src/runtime/asm_amd64.s:1695
```

The issue can be seen in Intellij IDEA and VSCode, latest versions. Below are the versions used,

GO version - v1.24.0
Delve version - v1.24.0

Discussion:
@aarzilli: Do you have a way that we can reproduce this problem?@kushicp: Hi @aarzilli, 

Steps to reproduce,

1. Create a go test case 
2. Set debug points
3. Debug the test case

Expected result
Able to debug successfully with control stopping at breakpoints

Actual Result:
Execution  hangs with control never arriving at the breakpoint@aarzilli: It doesn't happen. I don't see how it could happen in that case given that it's an error that can only happen while reading goroutine labels and a bare test case won't have any.@hongyuh-hh: Hi, @aarzilli , I also got this error. To reproduce it:

**Environment**

- OS: Darwin arm64
- Delve: v1.24.0+
- Go: v1.24.0+

**Step**

Run any debug session on the https://github.com/timandy/routine/tree/v1.1.4 repository that will invoke the `setLabels`: https://github.com/timandy/routine/blob/v1.1.4/goid.go#L49 

For example, debug this [test function](https://github.com/timandy/routine/blob/v1.1.4/thread_local_test.go#L33) and set breakpoint at [setLabels](https://github.com/timandy/routine/blob/v1.1.4/goid.go#L50)

You will find the debug session gets stuck, after click the `Stop`, IDE throw this panic:

```
org.jetbrains.concurrency.MessageError: Internal debugger error: <nil> not a String
runtime.gopanic (0x102419cf3)
	/opt/teamcity-agent/temp/buildTmp/go/src/runtime/panic.go:770
go/constant.StringVal (0x1025e07e3)
	/opt/teamcity-agent/temp/buildTmp/go/src/go/constant/value.go:500
github.com/go-delve/delve/pkg/proc.(*G).Labels (0x10278d82b)
	/opt/teamcity-agent/work/4c3e5e3eabfaf2d2/pkg/proc/variables.go:594
...
```@aarzilli: Fixed by #3968

Solution patch:
diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go
index d181387a..3d0ef859 100644
--- a/pkg/proc/core/core.go
+++ b/pkg/proc/core/core.go
@@ -319,18 +319,6 @@ func (t *thread) ProcessMemory() proc.MemoryReadWriter {
 	return t.p
 }
 
-// Location returns the location of this thread based on
-// the value of the instruction pointer register.
-func (t *thread) Location() (*proc.Location, error) {
-	regs, err := t.Registers()
-	if err != nil {
-		return nil, err
-	}
-	pc := regs.PC()
-	f, l, fn := t.p.bi.PCToLine(pc)
-	return &proc.Location{PC: pc, File: f, Line: l, Fn: fn}, nil
-}
-
 // Breakpoint returns the current breakpoint this thread is stopped at.
 // For core files this always returns an empty BreakpointState struct, as
 // there are no breakpoints when debugging core files.
diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go
index 9b4f8c85..856d8b05 100644
--- a/pkg/proc/fncall.go
+++ b/pkg/proc/fncall.go
@@ -815,7 +815,7 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
 	regval := bi.Arch.RegistersToDwarfRegisters(0, regs).Uint64Val(fncall.protocolReg)
 
 	if logflags.FnCall() {
-		loc, _ := thread.Location()
+		loc, _ := ThreadLocation(thread)
 		var pc uint64
 		var fnname string
 		if loc != nil {
@@ -1151,7 +1151,7 @@ func callInjectionProtocol(t *Target, trapthread Thread, threads []Thread) (done
 		t.currentThread = currentThread
 	}()
 	for _, thread := range threads {
-		loc, err := thread.Location()
+		loc, err := ThreadLocation(thread)
 		if err != nil {
 			continue
 		}
diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go
index 35d7091d..b58d0387 100644
--- a/pkg/proc/gdbserial/gdbserver.go
+++ b/pkg/proc/gdbserial/gdbserver.go
@@ -1523,22 +1523,6 @@ func (t *gdbThread) ProcessMemory() proc.MemoryReadWriter {
 	return t.p
 }
 
-// Location returns the current location of this thread.
-func (t *gdbThread) Location() (*proc.Location, error) {
-	regs, err := t.Registers()
-	if err != nil {
-		return nil, err
-	}
-	if pcreg, ok := regs.(*gdbRegisters).regs[regs.(*gdbRegisters).regnames.PC]; !ok {
-		t.p.conn.log.Errorf("thread %d could not find RIP register", t.ID)
-	} else if len(pcreg.value) < t.p.bi.Arch.PtrSize() {
-		t.p.conn.log.Errorf("thread %d bad length for RIP register: %d", t.ID, len(pcreg.value))
-	}
-	pc := regs.PC()
-	f, l, fn := t.p.bi.PCToLine(pc)
-	return &proc.Location{PC: pc, File: f, Line: l, Fn: fn}, nil
-}
-
 // Breakpoint returns the current active breakpoint for this thread.
 func (t *gdbThread) Breakpoint() *proc.BreakpointState {
 	return &t.CurrentBreakpoint
diff --git a/pkg/proc/native/threads.go b/pkg/proc/native/threads.go
index daf38d71..56ae4529 100644
--- a/pkg/proc/native/threads.go
+++ b/pkg/proc/native/threads.go
@@ -77,18 +77,6 @@ func (procgrp *processGroup) stepInstruction(t *nativeThread) (err error) {
 	return nil
 }
 
-// Location returns the threads location, including the file:line
-// of the corresponding source code, the function we're in
-// and the current instruction address.
-func (t *nativeThread) Location() (*proc.Location, error) {
-	pc, err := t.PC()
-	if err != nil {
-		return nil, err
-	}
-	f, l, fn := t.dbp.bi.PCToLine(pc)
-	return &proc.Location{PC: pc, File: f, Line: l, Fn: fn}, nil
-}
-
 // BinInfo returns information on the binary.
 func (t *nativeThread) BinInfo() *proc.BinaryInfo {
 	return t.dbp.bi
diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go
index dc716457..8839279a 100644
--- a/pkg/proc/target_exec.go
+++ b/pkg/proc/target_exec.go
@@ -444,7 +444,7 @@ func stepInstructionOut(grp *TargetGroup, dbp *Target, curthread Thread, fnname1
 		if err := grp.procgrp.StepInstruction(curthread.ThreadID()); err != nil {
 			return err
 		}
-		loc, err := curthread.Location()
+		loc, err := ThreadLocation(curthread)
 		var locFnName string
 		if loc.Fn != nil && !loc.Fn.cu.image.Stripped() {
 			locFnName = loc.Fn.Name
@@ -995,7 +995,7 @@ func stepIntoCallback(curthread Thread, p *Target) (bool, error) {
 	}
 
 	var fn *Function
-	if loc, _ := curthread.Location(); loc != nil {
+	if loc, _ := ThreadLocation(curthread); loc != nil {
 		fn = loc.Fn
 	}
 	g, _ := GetG(curthread)
@@ -1675,7 +1675,7 @@ func (t *Target) handleHardcodedBreakpoints(grp *TargetGroup, trapthread Thread,
 			continue
 		}
 
-		loc, err := thread.Location()
+		loc, err := ThreadLocation(thread)
 		if err != nil || loc.Fn == nil {
 			continue
 		}
diff --git a/pkg/proc/threads.go b/pkg/proc/threads.go
index d349c30a..4fb78b3b 100644
--- a/pkg/proc/threads.go
+++ b/pkg/proc/threads.go
@@ -8,7 +8,6 @@ import (
 
 // Thread represents a thread.
 type Thread interface {
-	Location() (*Location, error)
 	// Breakpoint will return the breakpoint that this thread is stopped at or
 	// nil if the thread is not stopped at any breakpoint.
 	Breakpoint() *BreakpointState
@@ -103,3 +102,16 @@ func setClosureReg(thread Thread, newClosureReg uint64) error {
 func setLR(thread Thread, newLR uint64) error {
 	return thread.SetReg(thread.BinInfo().Arch.LRRegNum, op.DwarfRegisterFromUint64(newLR))
 }
+
+// ThreadLocation returns the threads location, including the file:line
+// of the corresponding source code, the function we're in
+// and the current instruction address.
+func ThreadLocation(thread Thread) (*Location, error) {
+	regs, err := thread.Registers()
+	if err != nil {
+		return nil, err
+	}
+	pc := regs.PC()
+	f, l, fn := thread.BinInfo().PCToLine(pc)
+	return &Location{PC: pc, File: f, Line: l, Fn: fn}, nil
+}
diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go
index 6be6c1e3..d457b3fe 100644
--- a/pkg/proc/variables.go
+++ b/pkg/proc/variables.go
@@ -43,6 +43,8 @@ const (
 	maxMapBucketsFactor = 100 // Maximum numbers of map buckets to read for every requested map entry when loading variables through (*EvalScope).LocalVariables and (*EvalScope).FunctionArguments.
 
 	maxGoroutineUserCurrentDepth = 30 // Maximum depth used by (*G).UserCurrent to search its location
+
+	maxGoroutinesLabelEntries = 1_000 // Maximum number of label entries for a goroutine's label map
 )
 
 type floatSpecial uint8
@@ -257,7 +259,8 @@ func GetG(thread Thread) (*G, error) {
 	if thread.Common().g != nil {
 		return thread.Common().g, nil
 	}
-	if loc, _ := thread.Location(); loc != nil && loc.Fn != nil && loc.Fn.Name == "runtime.clone" {
+	loc, _ := ThreadLocation(thread)
+	if loc != nil && loc.Fn != nil && loc.Fn.Name == "runtime.clone" {
 		// When threads are executing runtime.clone the value of TLS is unreliable.
 		return nil, nil
 	}
@@ -296,7 +299,7 @@ func GetG(thread Thread) (*G, error) {
 		g.SystemStack = true
 	}
 	g.Thread = thread
-	if loc, err := thread.Location(); err == nil {
+	if err == nil {
 		g.CurrentLoc = *loc
 	}
 	thread.Common().g = g
@@ -354,7 +357,7 @@ func GoroutinesInfo(dbp *Target, start, count int) ([]*G, int, error) {
 			continue
 		}
 		if thg, allocated := threadg[g.ID]; allocated {
-			loc, err := thg.Thread.Location()
+			loc, err := ThreadLocation(thg.Thread)
 			if err != nil {
 				return nil, -1, err
 			}
@@ -584,6 +587,13 @@ func (g *G) Labels() map[string]string {
 				case reflect.Struct:
 					labelMap, _ = labelMap.structMember("list")
 					if labelMap != nil {
+						// ensure the labelMap.Len is a valid value to prevent infinite loops
+						if labelMap.Len < 0 {
+							labelMap.Len = 0
+						}
+						if labelMap.Len > maxGoroutinesLabelEntries {
+							labelMap.Len = maxGoroutinesLabelEntries
+						}
 						for i := int64(0); i < labelMap.Len; i++ {
 							v, err := labelMap.sliceAccess(int(i))
 							if err != nil {
diff --git a/service/api/conversions.go b/service/api/conversions.go
index fb90e414..14bf025a 100644
--- a/service/api/conversions.go
+++ b/service/api/conversions.go
@@ -97,7 +97,7 @@ func ConvertThread(th proc.Thread, bp *Breakpoint) *Thread {
 		gid      int64
 	)
 
-	loc, err := th.Location()
+	loc, err := proc.ThreadLocation(th)
 	if err == nil {
 		pc = loc.PC
 		file = loc.File
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index ba88e44c..ba73428c 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -989,6 +989,9 @@ func (d *Debugger) findBreakpointByName(name string) *api.Breakpoint {
 
 // CreateWatchpoint creates a watchpoint on the specified expression.
 func (d *Debugger) CreateWatchpoint(goid int64, frame, deferredCall int, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
+	d.targetMutex.Lock()
+	defer d.targetMutex.Unlock()
+
 	p := d.target.Selected
 
 	s, err := proc.ConvertEvalScope(p, goid, frame, deferredCall)
@@ -1890,7 +1893,7 @@ func (d *Debugger) CurrentPackage() (string, error) {
 	if _, err := d.target.Valid(); err != nil {
 		return "", err
 	}
-	loc, err := d.target.Selected.CurrentThread().Location()
+	loc, err := proc.ThreadLocation(d.target.Selected.CurrentThread())
 	if err != nil {
 		return "", err
 	}
@@ -2228,7 +2231,7 @@ func (d *Debugger) TargetGroup() *proc.TargetGroup {
 }
 
 func (d *Debugger) BuildID() string {
-	loc, err := d.target.Selected.CurrentThread().Location()
+	loc, err := proc.ThreadLocation(d.target.Selected.CurrentThread())
 	if err != nil {
 		return ""
 	}



Test patch:
diff --git a/pkg/proc/gdbserial/rr_test.go b/pkg/proc/gdbserial/rr_test.go
index 3dc3d1e2..23fdd6dd 100644
--- a/pkg/proc/gdbserial/rr_test.go
+++ b/pkg/proc/gdbserial/rr_test.go
@@ -73,7 +73,7 @@ func TestRestartAfterExit(t *testing.T) {
 		p := grp.Selected
 		setFunctionBreakpoint(p, t, "main.main")
 		assertNoError(grp.Continue(), t, "Continue")
-		loc, err := p.CurrentThread().Location()
+		loc, err := proc.ThreadLocation(p.CurrentThread())
 		assertNoError(err, t, "CurrentThread().Location()")
 		err = grp.Continue()
 		if !errors.As(err, &proc.ErrProcessExited{}) {
@@ -83,7 +83,7 @@ func TestRestartAfterExit(t *testing.T) {
 		assertNoError(grp.Restart(""), t, "Restart")
 
 		assertNoError(grp.Continue(), t, "Continue (after restart)")
-		loc2, err := p.CurrentThread().Location()
+		loc2, err := proc.ThreadLocation(p.CurrentThread())
 		assertNoError(err, t, "CurrentThread().Location() (after restart)")
 		if loc2.Line != loc.Line {
 			t.Fatalf("stopped at %d (expected %d)", loc2.Line, loc.Line)
@@ -101,13 +101,13 @@ func TestRestartDuringStop(t *testing.T) {
 		p := grp.Selected
 		setFunctionBreakpoint(p, t, "main.main")
 		assertNoError(grp.Continue(), t, "Continue")
-		loc, err := p.CurrentThread().Location()
+		loc, err := proc.ThreadLocation(p.CurrentThread())
 		assertNoError(err, t, "CurrentThread().Location()")
 
 		assertNoError(grp.Restart(""), t, "Restart")
 
 		assertNoError(grp.Continue(), t, "Continue (after restart)")
-		loc2, err := p.CurrentThread().Location()
+		loc2, err := proc.ThreadLocation(p.CurrentThread())
 		assertNoError(err, t, "CurrentThread().Location() (after restart)")
 		if loc2.Line != loc.Line {
 			t.Fatalf("stopped at %d (expected %d)", loc2.Line, loc.Line)
@@ -143,7 +143,7 @@ func TestReverseBreakpointCounts(t *testing.T) {
 		p := grp.Selected
 		endbp := setFileBreakpoint(p, t, fixture, 28)
 		assertNoError(grp.Continue(), t, "Continue()")
-		loc, _ := p.CurrentThread().Location()
+		loc, _ := proc.ThreadLocation(p.CurrentThread())
 		if loc.PC != endbp.Addr {
 			t.Fatalf("did not reach end of main.main function: %s:%d (%#x)", loc.File, loc.Line, loc.PC)
 		}
@@ -156,7 +156,7 @@ func TestReverseBreakpointCounts(t *testing.T) {
 	countLoop:
 		for {
 			assertNoError(grp.Continue(), t, "Continue()")
-			loc, _ := p.CurrentThread().Location()
+			loc, _ := proc.ThreadLocation(p.CurrentThread())
 			switch loc.PC {
 			case startbp.Addr:
 				break countLoop
@@ -188,7 +188,7 @@ func getPosition(grp *proc.TargetGroup, t *testing.T) (when string, loc *proc.Lo
 	var err error
 	when, err = grp.When()
 	assertNoError(err, t, "When")
-	loc, err = grp.Selected.CurrentThread().Location()
+	loc, err = proc.ThreadLocation(grp.Selected.CurrentThread())
 	assertNoError(err, t, "Location")
 	return
 }
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 9ecfec7e..e8de4107 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -1627,7 +1627,7 @@ func TestStepIntoFunction(t *testing.T) {
 		// Step into function
 		assertNoError(grp.Step(), t, "Step() returned an error")
 		// We should now be inside the function.
-		loc, err := p.CurrentThread().Location()
+		loc, err := proc.ThreadLocation(p.CurrentThread())
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -2462,7 +2462,7 @@ func TestNextInDeferReturn(t *testing.T) {
 		// point where the target program panics.
 		setFunctionBreakpoint(p, t, "main.sampleFunction")
 		for i := 0; i < 20; i++ {
-			loc, err := p.CurrentThread().Location()
+			loc, err := proc.ThreadLocation(p.CurrentThread())
 			assertNoError(err, t, "CurrentThread().Location()")
 			t.Logf("at %#x %s:%d", loc.PC, loc.File, loc.Line)
 			if loc.Fn != nil && loc.Fn.Name == "main.sampleFunction" {
@@ -3184,7 +3184,7 @@ func TestIssue1008(t *testing.T) {
 	withTestProcess("cgostacktest/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
 		setFunctionBreakpoint(p, t, "main.main")
 		assertNoError(grp.Continue(), t, "Continue()")
-		loc, err := p.CurrentThread().Location()
+		loc, err := proc.ThreadLocation(p.CurrentThread())
 		assertNoError(err, t, "CurrentThread().Location()")
 		t.Logf("location %v\n", loc)
 		if !strings.HasSuffix(loc.File, "/main.go") {
@@ -5176,7 +5176,7 @@ func TestFollowExec(t *testing.T) {
 				if grp.Selected.CurrentThread().Breakpoint().Breakpoint.LogicalID() != 1 {
 					t.Fatalf("wrong breakpoint %#v", grp.Selected.CurrentThread().Breakpoint().Breakpoint)
 				}
-				loc, err := grp.Selected.CurrentThread().Location()
+				loc, err := proc.ThreadLocation(grp.Selected.CurrentThread())
 				assertNoError(err, t, "Location")
 				if loc.Fn.Name != "main.traceme1" {
 					t.Fatalf("wrong stop location %#v", loc)
@@ -5188,7 +5188,7 @@ func TestFollowExec(t *testing.T) {
 				if p.CurrentThread().Breakpoint().Breakpoint.LogicalID() != 3 {
 					t.Fatalf("wrong breakpoint %#v", p.CurrentThread().Breakpoint().Breakpoint)
 				}
-				loc, err := p.CurrentThread().Location()
+				loc, err := proc.ThreadLocation(p.CurrentThread())
 				assertNoError(err, t, "Location")
 				if loc.Fn.Name != "main.traceme3" {
 					t.Fatalf("wrong stop location %#v", loc)
@@ -5208,7 +5208,7 @@ func TestFollowExec(t *testing.T) {
 						t.Fatalf("wrong breakpoint %#v", grp.Selected.CurrentThread().Breakpoint().Breakpoint)
 					}
 					pids[tgt.Pid()]++
-					loc, err := tgt.CurrentThread().Location()
+					loc, err := proc.ThreadLocation(tgt.CurrentThread())
 					assertNoError(err, t, "Location")
 					if loc.Fn.Name != "main.traceme2" {
 						t.Fatalf("wrong stop location %#v", loc)
@@ -5268,7 +5268,7 @@ func TestStepShadowConcurrentBreakpoint(t *testing.T) {
 		for {
 			t.Logf("stop (%d %d):", stacktraceme1calls, stacktraceme2calls)
 			for _, th := range p.ThreadList() {
-				loc, _ := th.Location()
+				loc, _ := proc.ThreadLocation(th)
 				t.Logf("\t%s:%d\n", loc.File, loc.Line)
 				bp := th.Breakpoint().Breakpoint
 				if bp != nil && bp.Addr == break2.Addr {
diff --git a/pkg/proc/variables_fuzz_test.go b/pkg/proc/variables_fuzz_test.go
index 9f7aac1b..6431981d 100644
--- a/pkg/proc/variables_fuzz_test.go
+++ b/pkg/proc/variables_fuzz_test.go
@@ -136,7 +136,7 @@ func doFuzzEvalExpressionSetup(f *testing.F) {
 	assertNoError(err, f, "Stacktrace")
 
 	mem := c.Memory()
-	loc, _ := c.CurrentThread().Location()
+	loc, _ := proc.ThreadLocation(c.CurrentThread())
 	tmem := &tracingMem{make(map[uint64]int), mem}
 
 	scope := &proc.EvalScope{Location: *loc, Regs: frames[0].Regs, Mem: tmem, BinInfo: c.BinInfo()}