Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ localtests
.teamcity/target
.vscode
.claude
.superpowers/
docs/superpowers/
dist/
32 changes: 29 additions & 3 deletions pkg/proc/fncall.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ type functionCallState struct {
// undoInjection is set after evalop.CallInjectionSetTarget runs and cleared by evalCallInjectionComplete
// it contains information on how to undo a function call injection without running it
undoInjection *undoInjection
// targetSet is true after CallInjectionSetTarget runs. Unlike undoInjection it
// is not cleared by CallInjectionComplete, so RestoreRegisters can tell whether
// the target was ever configured.
targetSet bool

// hasDebugPinner is true if the target has runtime.debugPinner
hasDebugPinner bool
Expand Down Expand Up @@ -807,6 +811,20 @@ const (
debugCallRegRestoreRegisters = 16
)

// checkCallInjectionReadyToFinish records an error if the call injection
// protocol is finishing without CallInjectionSetTarget having run.
// Precheck failures never reach SetTarget but already set fncall.err, so
// those are left alone.
func checkCallInjectionReadyToFinish(fncall *functionCallState) {
if !fncall.targetSet && fncall.err == nil {
fncall.err = errors.New("call injection terminated before target was set")
}
}

func unknownProtocolRegisterError(regval uint64) error {
return fmt.Errorf("unknown value of protocol register %#x", regval)
}

// funcCallStep executes one step of the function call injection protocol.
func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
p := callScope.callCtx.p
Expand Down Expand Up @@ -858,6 +876,7 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
case debugCallRegRestoreRegisters: // 16
// runtime requests that we restore the registers (all except pc and sp),
// this is also the last step of the function call protocol.
checkCallInjectionReadyToFinish(fncall)
pc, sp := regs.PC(), regs.SP()
if err := thread.RestoreRegisters(fncall.savedRegs); err != nil {
fncall.err = fmt.Errorf("could not restore registers: %v", err)
Expand Down Expand Up @@ -957,10 +976,12 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
fncall.panicvar.Name = "~panic"

default:
// Got an unknown protocol register value, this is probably bad but the safest thing
// possible is to ignore it and hope it didn't matter.
// Unknown protocol register value: do not continue blindly. Continuing
// without SetTarget can finish the call and then panic in
// evalCallInjectionSetTarget (see issues #4085, #4363).
fncall.err = unknownProtocolRegisterError(regval)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could just be the fmt.Errorf call directly.

stack.callInjectionContinue = true
fncallLog("unknown value of protocol register %#x", regval)
fncallLog("%v", fncall.err)
}

return false
Expand All @@ -987,6 +1008,10 @@ func callInjectionComplete2(callScope *EvalScope, bi *BinaryInfo, fncall *functi
}

func (scope *EvalScope) evalCallInjectionSetTarget(op *evalop.CallInjectionSetTarget, stack *evalStack, thread Thread) {
if len(stack.fncalls) == 0 {
stack.err = errors.New("CallInjectionSetTarget without active call injection")
return
}
fncall := stack.fncallPeek()
if !fncall.hasDebugPinner && (fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0) {
stack.err = funcCallEvalFuncExpr(scope, stack, fncall)
Expand Down Expand Up @@ -1016,6 +1041,7 @@ func (scope *EvalScope) evalCallInjectionSetTarget(op *evalop.CallInjectionSetTa
callOP(scope.BinInfo, thread, regs, fncall.fn.Entry)

fncall.undoInjection = undo
fncall.targetSet = true

if fncall.receiver != nil {
err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], thread)
Expand Down
198 changes: 198 additions & 0 deletions pkg/proc/fncall_protocol_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Post-Start call-injection protocol harness for issues #4085 / #4363.
//
// Seeds a functionCallState as if CallInjectionStart succeeded, then feeds
// protocol-register values into funcCallStep / resume. Premature
// RestoreRegisters (16) finishes the call before SetTarget and must not panic.
//
// Broader opcode / full-register fuzzing lives on fix/eval-stack-fuzz.

package proc

import (
"errors"
"fmt"
"go/constant"
"strings"
"testing"

"github.com/go-delve/delve/pkg/dwarf/op"
"github.com/go-delve/delve/pkg/dwarf/regnum"
"github.com/go-delve/delve/pkg/proc/evalop"
)

const (
fncallProtoPC = 0x2000
fncallProtoSP = 0x1000
)

type fncallProtoMem struct{}

func (*fncallProtoMem) ReadMemory(b []byte, _ uint64) (int, error) {
clear(b)
return len(b), nil
}
func (*fncallProtoMem) WriteMemory(_ uint64, b []byte) (int, error) { return len(b), nil }

type fncallProtoRegs struct {
protocolReg, regval uint64
}

func (r *fncallProtoRegs) PC() uint64 { return fncallProtoPC }
func (r *fncallProtoRegs) SP() uint64 { return fncallProtoSP }
func (r *fncallProtoRegs) BP() uint64 { return fncallProtoSP }
func (r *fncallProtoRegs) LR() uint64 { return 0 }
func (r *fncallProtoRegs) TLS() uint64 { return 0 }
func (r *fncallProtoRegs) GAddr() (uint64, bool) { return 0, false }
func (r *fncallProtoRegs) Slice(bool) ([]Register, error) {
return AppendUint64Register(
AppendUint64Register(
AppendUint64Register(nil, regnum.AMD64ToName(regnum.AMD64_Rip), fncallProtoPC),
regnum.AMD64ToName(regnum.AMD64_Rsp), fncallProtoSP),
regnum.AMD64ToName(r.protocolReg), r.regval), nil
}
func (r *fncallProtoRegs) Copy() (Registers, error) { cp := *r; return &cp, nil }

type fncallProtoThread struct {
bi *BinaryInfo
mem MemoryReadWriter
protocolReg uint64
regvals []uint64
step int
common CommonThread
}

func (th *fncallProtoThread) regval() uint64 {
if th.step >= len(th.regvals) {
return 0xdead
}
return th.regvals[th.step]
}
func (th *fncallProtoThread) Breakpoint() *BreakpointState { return &BreakpointState{} }
func (th *fncallProtoThread) ThreadID() int { return 1 }
func (th *fncallProtoThread) Registers() (Registers, error) {
return &fncallProtoRegs{protocolReg: th.protocolReg, regval: th.regval()}, nil
}
func (th *fncallProtoThread) RestoreRegisters(Registers) error { return nil }
func (th *fncallProtoThread) BinInfo() *BinaryInfo { return th.bi }
func (th *fncallProtoThread) ProcessMemory() MemoryReadWriter { return th.mem }
func (th *fncallProtoThread) SetCurrentBreakpoint(bool) error { return nil }
func (th *fncallProtoThread) SoftExc() bool { return false }
func (th *fncallProtoThread) Common() *CommonThread { return &th.common }
func (th *fncallProtoThread) SetReg(uint64, *op.DwarfRegister) error { return nil }

type fncallProtoProcess struct {
bi *BinaryInfo
mem MemoryReadWriter
}

func (p *fncallProtoProcess) BinInfo() *BinaryInfo { return p.bi }
func (p *fncallProtoProcess) EntryPoint() (uint64, error) { return 0, nil }
func (p *fncallProtoProcess) FindThread(int) (Thread, bool) { return nil, false }
func (p *fncallProtoProcess) ThreadList() []Thread { return nil }
func (p *fncallProtoProcess) Breakpoints() *BreakpointMap { return nil }
func (p *fncallProtoProcess) Memory() MemoryReadWriter { return p.mem }

type fncallProtoProcessGroup struct{}

func (fncallProtoProcessGroup) ContinueOnce(*ContinueOnceContext) (Thread, StopReason, error) {
return nil, StopUnknown, errors.New("no live target in protocol harness")
}
func (fncallProtoProcessGroup) StepInstruction(int) error { return nil }
func (fncallProtoProcessGroup) Detach(int, bool) error { return nil }
func (fncallProtoProcessGroup) Close() error { return nil }

func runCallInjectionProtocol(regvals []uint64) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("internal debugger error: panic: %v", r)
}
}()

bi := NewBinaryInfo("linux", "amd64")
th := &fncallProtoThread{
bi: bi, mem: &fncallProtoMem{}, protocolReg: regnum.AMD64_R12, regvals: regvals,
}
mem := th.mem
tgt := &Target{
Process: &fncallProtoProcess{bi: bi, mem: mem},
fncallForG: map[int64]*callInjection{},
}
g := &G{ID: 1}
tgt.fncallForG[g.ID] = &callInjection{startThreadID: th.ThreadID()}
scope := &EvalScope{
Mem: mem, BinInfo: bi, g: g, target: tgt,
callCtx: &callContext{grp: &TargetGroup{procgrp: fncallProtoProcessGroup{}}, p: tgt},
}
stack := &evalStack{scope: scope, curthread: th}
stack.fncallPush(&functionCallState{
protocolReg: th.protocolReg,
debugCallName: "runtime.debugCallV2",
savedRegs: &fncallProtoRegs{protocolReg: th.protocolReg},
hasDebugPinner: true,
fn: &Function{Entry: fncallProtoPC},
})
stack.push(newConstant(constant.MakeBool(false), bi, mem))
stack.ops = []evalop.Op{
&evalop.CallInjectionSetTarget{},
&evalop.CallInjectionComplete{DoPinning: false},
}

for step := 0; step < len(regvals)+2; step++ {
th.step = step
if finished := funcCallStep(scope, stack, th); finished {
funcCallFinish(scope, stack)
}
if stack.err == nil && len(stack.fncalls) > 0 {
if fncall := stack.fncallPeek(); fncall.err != nil {
stack.err = fncall.err
}
}
if stack.callInjectionContinue {
stack.callInjectionContinue = false
continue
}
if stack.err != nil {
break
}
// Like resume: continue opcode execution after the protocol step.
stack.run()
if !stack.callInjectionContinue {
break
}
stack.callInjectionContinue = false
}
return stack.err
}

func failIfInternalDebuggerError(t testing.TB, err error) {
t.Helper()
if err != nil && strings.Contains(err.Error(), "internal debugger error") {
t.Fatalf("unexpected internal debugger error: %v", err)
}
}

func TestCallInjectionProtocol(t *testing.T) {
tests := []struct {
regs []uint64
wantSubstr string // if non-empty, err must contain this
}{
{[]uint64{16}, "terminated before target"}, // premature restore (#4085/#4363)
{[]uint64{0x42, 16}, ""}, // unknown then restore
{[]uint64{0, 16}, ""}, // complete-call then restore
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%v", tt.regs), func(t *testing.T) {
err := runCallInjectionProtocol(tt.regs)
failIfInternalDebuggerError(t, err)
if tt.wantSubstr == "" {
return
}
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), tt.wantSubstr) {
t.Fatalf("expected %q in error, got: %v", tt.wantSubstr, err)
}
})
}
}
53 changes: 53 additions & 0 deletions pkg/proc/proc_unexported_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package proc

import (
"errors"
"testing"

"github.com/go-delve/delve/pkg/proc/evalop"
)

func TestAlignAddr(t *testing.T) {
Expand Down Expand Up @@ -96,3 +99,53 @@ func TestConvertInt(t *testing.T) {
}
}
}

// TestEvalCallInjectionSetTarget_EmptyFncalls_ReturnsError verifies that
// CallInjectionSetTarget does not panic when the fncalls stack is empty
// (telemetry issues #4085, #4363).
func TestEvalCallInjectionSetTarget_EmptyFncalls_ReturnsError(t *testing.T) {
scope := &EvalScope{}
stack := &evalStack{}
defer func() {
if r := recover(); r != nil {
t.Fatalf("evalCallInjectionSetTarget panicked: %v", r)
}
}()
scope.evalCallInjectionSetTarget(&evalop.CallInjectionSetTarget{}, stack, nil)
if stack.err == nil {
t.Fatal("expected error when CallInjectionSetTarget runs with no active call injection")
}
}

func TestCheckCallInjectionReadyToFinish(t *testing.T) {
t.Run("errorsWhenTargetNeverSet", func(t *testing.T) {
fncall := &functionCallState{}
checkCallInjectionReadyToFinish(fncall)
if fncall.err == nil {
t.Fatal("expected error when finishing a call that never ran SetTarget")
}
})
t.Run("preservesPrecheckError", func(t *testing.T) {
precheckErr := errors.New("call not allowed at this point")
fncall := &functionCallState{err: precheckErr}
checkCallInjectionReadyToFinish(fncall)
if !errors.Is(fncall.err, precheckErr) {
t.Fatalf("expected precheck error to be preserved, got %v", fncall.err)
}
})
t.Run("okWhenTargetWasSet", func(t *testing.T) {
fncall := &functionCallState{targetSet: true}
checkCallInjectionReadyToFinish(fncall)
if fncall.err != nil {
t.Fatalf("unexpected error when SetTarget had run: %v", fncall.err)
}
})
t.Run("okWhenTargetWasSetEvenIfUndoCleared", func(t *testing.T) {
// CallInjectionComplete clears undoInjection on success; targetSet must remain.
fncall := &functionCallState{targetSet: true, undoInjection: nil}
checkCallInjectionReadyToFinish(fncall)
if fncall.err != nil {
t.Fatalf("unexpected error after Complete cleared undoInjection: %v", fncall.err)
}
})
}