Repository: go-delve/delve

Issue:
Random title of exe file causes problems to windows firewall
1. What version of Delve are you using (`dlv version`)? 1.21.0
2. What version of Go are you using? (`go version`)? 1.20.7
3. What operating system and processor architecture are you using? Windows 10 x64
4. What did you do?
5. What did you expect to see?
6. What did you see instead?

In a recent release, delve temporary exe file became random (__debug_binXXXXX.exe where X are random numbers).

This causes problem with windows firewall because each new debug session creates a rule in windows firewall.
![image](https://github.com/go-delve/delve/assets/1791230/c64d9670-f3ef-48a1-afc0-4ec9ae80430a)

A nice behaviour IMO is that delve first checks if there is already a file __debug_bin.exe, if no, then compile normally to __debug_bin.exe, if it exists, add an incremental value to file name.

Discussion:
@matwachich: https://github.com/go-delve/delve/pull/3366@rustyoz: @matwachich Can you give the latest master version a try? @aarzilli: Closing, this should be fixed.

Solution patch:
diff --git a/_fixtures/issue4000.go b/_fixtures/issue4000.go
new file mode 100644
index 00000000..d83eaeb5
--- /dev/null
+++ b/_fixtures/issue4000.go
@@ -0,0 +1,14 @@
+package main
+
+import "fmt"
+
+func main() {
+	var test = "a string"
+	var f = func() {
+		test = "another string"
+		fmt.Println("test", test)
+	}
+	fmt.Println(test)
+	f()
+	fmt.Println(test)
+}
diff --git a/pkg/dwarf/reader/variables.go b/pkg/dwarf/reader/variables.go
index d76164bd..ae48bc93 100644
--- a/pkg/dwarf/reader/variables.go
+++ b/pkg/dwarf/reader/variables.go
@@ -60,12 +60,22 @@ func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, l
 		// they are declared on does not matter; they are considered to be
 		// visible throughout the function.
 		declLine, varHasDeclLine := root.Val(dwarf.AttrDeclLine).(int64)
+
 		checkDeclLine :=
 			root.Tag != dwarf.TagFormalParameter && // var is not a function argument
 				varHasDeclLine && // we know the DeclLine
 				(flags&VariablesNoDeclLineCheck == 0) // we were not explicitly instructed to ignore DeclLine
 
-		varVisible := !checkDeclLine || (line >= int(declLine)+1) // +1 because visibility starts on the line after DeclLine
+		varVisible := true
+		if checkDeclLine {
+			varIsCaptured := root.Val(godwarf.AttrGoClosureOffset) != nil
+			if varIsCaptured {
+				varVisible = line >= int(declLine)
+			} else {
+				varVisible = line >= int(declLine)+1 // +1 because visibility starts on the line after DeclLine
+			}
+		}
+
 		if varVisible {
 			return append(v, Variable{root, depth})
 		}
diff --git a/pkg/gobuild/defaultexe.go b/pkg/gobuild/defaultexe.go
index e15684bb..da7d1ec4 100644
--- a/pkg/gobuild/defaultexe.go
+++ b/pkg/gobuild/defaultexe.go
@@ -11,9 +11,21 @@ import (
 // directory named 'name' followed by a random string
 func DefaultDebugBinaryPath(name string) string {
 	pattern := name
+
 	if runtime.GOOS == "windows" {
-		pattern += "*.exe"
+		pattern += ".exe"
+		// Try to create the file atomically with O_EXCL to avoid race conditions
+		f, err := os.OpenFile(pattern, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+		if err == nil {
+			// Successfully created the file
+			r := f.Name()
+			f.Close()
+			return r
+		}
+		// If file already exists or other error, fall back to CreateTemp
+		// (we don't need to specifically check os.IsExist because CreateTemp will handle any case)
 	}
+
 	f, err := os.CreateTemp(".", pattern)
 	if err != nil {
 		logflags.DebuggerLogger().Errorf("could not create temporary file for build output: %v", err)
diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go
index 3a0672c0..ed7815f1 100644
--- a/pkg/proc/breakpoints.go
+++ b/pkg/proc/breakpoints.go
@@ -1039,7 +1039,7 @@ func (rbpi *returnBreakpointInfo) Collect(t *Target, thread Thread) []*Variable
 		return returnInfoError("could not read function entry", err, thread.BinInfo(), thread.ProcessMemory())
 	}
 
-	vars, err := scope.Locals(0, "")
+	vars, err := scope.Locals(localsFakeFunctionEntryScope, "")
 	if err != nil {
 		return returnInfoError("could not evaluate return variables", err, thread.BinInfo(), thread.ProcessMemory())
 	}
diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go
index 3daa8847..346741fe 100644
--- a/pkg/proc/eval.go
+++ b/pkg/proc/eval.go
@@ -74,6 +74,9 @@ const (
 	// If localsIsRangeBody is set DW_AT_formal_parameter variables will be
 	// considered local variables.
 	localsIsRangeBody
+
+	// If localsFakeFunctionEntryScope is set the current scope was generated by fakeFunctionEntryScope
+	localsFakeFunctionEntryScope
 )
 
 // ConvertEvalScope returns a new EvalScope in the context of the
@@ -444,6 +447,8 @@ func (scope *EvalScope) simpleLocals(flags localsFlags, wantedName string) ([]*V
 		scope.dictAddr = readLocalPtrVar(dwarfTree, goDictionaryName, scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem)
 	}
 
+	var closureStruct *Variable
+
 	vars := make([]*Variable, 0, len(varEntries))
 	depths := make([]int, 0, len(varEntries))
 	for _, entry := range varEntries {
@@ -462,15 +467,18 @@ func (scope *EvalScope) simpleLocals(flags localsFlags, wantedName string) ([]*V
 				continue
 			}
 		}
-		if scope.Fn.rangeParentName() != "" && (strings.HasPrefix(name, "~") || entry.Val(godwarf.AttrGoClosureOffset) != nil) {
+		isCapturedVar := entry.Val(godwarf.AttrGoClosureOffset) != nil
+		if scope.Fn.rangeParentName() != "" && (strings.HasPrefix(name, "~") || isCapturedVar) {
 			// Skip unnamed parameters and closure variables for range-over-func closure bodies
 			continue
 		}
+
 		val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree, scope.dictAddr)
 		if err != nil {
 			// skip variables that we can't parse yet
 			continue
 		}
+
 		if trustArgOrder && ((val.Unreadable != nil && val.Addr == 0) || val.Flags&VariableFakeAddress != 0) && entry.Tag == dwarf.TagFormalParameter {
 			addr := afterLastArgAddr(vars)
 			if addr == 0 {
@@ -479,6 +487,39 @@ func (scope *EvalScope) simpleLocals(flags localsFlags, wantedName string) ([]*V
 			addr = uint64(alignAddr(int64(addr), val.DwarfType.Align()))
 			val = newVariable(val.Name, addr, val.DwarfType, scope.BinInfo, scope.Mem)
 		}
+
+		if isCapturedVar && int(val.DeclLine) == scope.Line && scope.Fn != nil && entry.Tag == dwarf.TagVariable && (flags&localsFakeFunctionEntryScope == 0) {
+			// For variables captured by closures if we are "early" in the function
+			// read  the value from the closure struct instead of their location.
+			// First check that we are actually between the entry point of the
+			// function and the end of the prologue in case the code has not been
+			// gofmt'd.
+			firstPCAfterPrologue := uint64(0)
+			if scope.Fn.cu.lineInfo != nil {
+				fn := scope.Fn
+				firstPCAfterPrologue, _, _, _ = fn.cu.lineInfo.PrologueEndPC(fn.Entry, fn.End)
+			}
+			if firstPCAfterPrologue < scope.Fn.Entry {
+				firstPCAfterPrologue = scope.Fn.Entry
+			}
+			if scope.PC >= scope.Fn.Entry && scope.PC <= firstPCAfterPrologue {
+				cst := scope.Fn.extra(scope.BinInfo).closureStructType
+				if closureStruct == nil {
+					closureStruct = newVariable(scope.Fn.Name, scope.Regs.Uint64Val(scope.BinInfo.Arch.ContextRegNum), cst, scope.BinInfo, scope.Mem)
+				}
+				for i := range cst.Field {
+					if cst.Field[i].Name == name {
+						val2, _ := closureStruct.toField(cst.Field[i])
+						if val2 != nil && val2.Unreadable == nil {
+							val2.Name = name
+							val = val2
+						}
+						break
+					}
+				}
+			}
+		}
+
 		vars = append(vars, val)
 		depth := entry.Depth
 		if (flags&localsIsRangeBody == 0) && (entry.Tag == dwarf.TagFormalParameter) {
@@ -1779,6 +1820,10 @@ func typeCastCompatibleTypes(typ1, typ2 godwarf.Type) bool {
 			}
 			return true
 		}
+	case *godwarf.SliceType:
+		if ttyp2, ok := typ2.(*godwarf.SliceType); ok {
+			return ttyp1.ElemType.String() == ttyp2.ElemType.String()
+		}
 	case *godwarf.ComplexType:
 		if _, ok := typ2.(*godwarf.ComplexType); ok {
 			// size and alignment already checked above
diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go
index 53adb797..0b00839f 100644
--- a/pkg/proc/fncall.go
+++ b/pkg/proc/fncall.go
@@ -893,7 +893,7 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
 		// pretend we are still inside the function we called
 		fakeFunctionEntryScope(retScope, fncall.fn, int64(regs.SP()), regs.SP()-uint64(bi.Arch.PtrSize()))
 		var flags localsFlags
-		flags |= localsNoDeclLineCheck // if the function we are calling is an autogenerated stub then declaration lines have no meaning
+		flags |= localsNoDeclLineCheck | localsFakeFunctionEntryScope // if the function we are calling is an autogenerated stub then declaration lines have no meaning
 		if !bi.regabi {
 			flags |= localsTrustArgOrder
 		}



Test patch:
diff --git a/_fixtures/testvariables2.go b/_fixtures/testvariables2.go
index a20d9fb7..f8907166 100644
--- a/_fixtures/testvariables2.go
+++ b/_fixtures/testvariables2.go
@@ -169,6 +169,8 @@ type ThreeInts struct {
 	a, b, c int
 }
 
+type Message []byte
+
 var _ I = (*W2)(nil)
 
 type pptr *pptr
@@ -423,6 +425,8 @@ func main() {
 	enum6 := FloatConst
 	var zeropoint4 float64 = 0.4
 
+	var messageVar Message = Message{1, 2, 3, 4, 5}
+
 	var amb1 = 1
 	runtime.Breakpoint()
 	for amb1 := 0; amb1 < 10; amb1++ {
@@ -433,5 +437,5 @@ func main() {
 	longslice := make([]int, 100, 100)
 
 	runtime.Breakpoint()
-	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge)
+	fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge, messageVar)
 }
diff --git a/pkg/proc/variables_test.go b/pkg/proc/variables_test.go
index 7f7ea842..513047d5 100644
--- a/pkg/proc/variables_test.go
+++ b/pkg/proc/variables_test.go
@@ -836,6 +836,7 @@ func getEvalExpressionTestCases() []varTest {
 		{"int8(i6)", false, "12", "12", "int8", nil},
 		{"string(byteslice[0])", false, `"t"`, `"t"`, "string", nil},
 		{"string(runeslice[0])", false, `"t"`, `"t"`, "string", nil},
+		{"[]uint8(messageVar)", false, `[]uint8 len: 5, cap: 5, [1,2,3,4,5]`, `[]uint8 len: 5, cap: 5, [...]`, "[]uint8", nil},
 
 		// misc
 		{"i1", true, "1", "1", "int", nil},
@@ -1967,3 +1968,36 @@ func TestCallFunctionRegisterArg(t *testing.T) {
 		assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "value.Type()", pnormalLoadConfig, true), t, "EvalExpressionWithCalls")
 	})
 }
+
+func TestCapturedVarVisibleOnFirstLine(t *testing.T) {
+	// Checks that a variable captured by a closure is visible on the first
+	// line of the closure function.
+	// See issue #4000
+	if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 23) {
+		t.Skip("not implemented")
+	}
+	skipOn(t, "broken", "linux", "386")
+	withTestProcess("issue4000", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
+		addrs, err := proc.FindFileLocation(p, fixture.Source, 7)
+		assertNoError(err, t, "FindFileLocation")
+		found := false
+		for _, addr := range addrs {
+			_, _, fn := p.BinInfo().PCToLine(addr)
+			if fn != nil && strings.HasPrefix(fn.Name, "main.main.") {
+				found = true
+				_, err := p.SetBreakpoint(int(addr), addr, proc.UserBreakpoint, nil)
+				assertNoError(err, t, "SetBreakpoint")
+			}
+		}
+		if !found {
+			t.Fatal("could not find main.main.func1 at :7")
+		}
+		assertNoError(grp.Continue(), t, "Continue()") // this stops inside main.main.func1
+		v := evalVariable(p, t, "test")
+		cv := api.ConvertVar(v)
+		t.Logf("test variable: %s", cv.SinglelineString())
+		if tgt, s := `"a string"`, cv.SinglelineString(); s != tgt {
+			t.Fatalf("test variable expected %q got %q", tgt, s)
+		}
+	})
+}