runtime: account for missing frame pointer in preamble

If a goroutine is synchronously preempted, then taking a
frame-pointer-based stack trace at that preemption will skip PC of the
caller of the function which called into morestack. This happens because
the frame pointer is pushed to the stack after the preamble, leaving the
stack in an odd state for frame pointer unwinding.

Deal with this by marking a goroutine as synchronously preempted and
using that signal to load the missing PC from the stack. On LR platforms
this is available in gp.sched.lr. On non-LR platforms like x86, it's at
gp.sched.sp, because there are no args, no locals, and no frame pointer
pushed to the SP yet.

For #68090.

Change-Id: I73a1206d8b84eecb8a96dbe727195da30088f288
Reviewed-on: https://go-review.googlesource.com/c/go/+/684435
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nick Ripley <nick.ripley@datadoghq.com>
This commit is contained in:
Michael Anthony Knyszek 2025-06-27 00:59:49 +00:00 committed by Michael Knyszek
parent fdc076ce76
commit 742fda9524
7 changed files with 47 additions and 9 deletions

View file

@ -97,6 +97,11 @@ func main() {
rp.Read(data[:]) rp.Read(data[:])
pipeReadDone <- true pipeReadDone <- true
}() }()
go func() { // func12
for {
syncPreemptPoint()
}
}()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
runtime.GC() runtime.GC()
@ -127,3 +132,12 @@ func main() {
runtime.GOMAXPROCS(oldGoMaxProcs) runtime.GOMAXPROCS(oldGoMaxProcs)
} }
//go:noinline
func syncPreemptPoint() {
if never {
syncPreemptPoint()
}
}
var never bool

View file

@ -326,7 +326,8 @@ func TestTraceStacks(t *testing.T) {
const mainLine = 21 const mainLine = 21
want := []evDesc{ want := []evDesc{
{trace.EventStateTransition, "Goroutine Running->Runnable", []frame{ {trace.EventStateTransition, "Goroutine Running->Runnable", []frame{
{"main.main", mainLine + 82}, {"runtime.Gosched", 0},
{"main.main", mainLine + 87},
}}, }},
{trace.EventStateTransition, "Goroutine NotExist->Runnable", []frame{ {trace.EventStateTransition, "Goroutine NotExist->Runnable", []frame{
{"main.main", mainLine + 11}, {"main.main", mainLine + 11},
@ -349,7 +350,7 @@ func TestTraceStacks(t *testing.T) {
}}, }},
{trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{ {trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{
{"runtime.chansend1", 0}, {"runtime.chansend1", 0},
{"main.main", mainLine + 84}, {"main.main", mainLine + 89},
}}, }},
{trace.EventStateTransition, "Goroutine Running->Waiting", []frame{ {trace.EventStateTransition, "Goroutine Running->Waiting", []frame{
{"runtime.chansend1", 0}, {"runtime.chansend1", 0},
@ -357,7 +358,7 @@ func TestTraceStacks(t *testing.T) {
}}, }},
{trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{ {trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{
{"runtime.chanrecv1", 0}, {"runtime.chanrecv1", 0},
{"main.main", mainLine + 85}, {"main.main", mainLine + 90},
}}, }},
{trace.EventStateTransition, "Goroutine Running->Waiting", []frame{ {trace.EventStateTransition, "Goroutine Running->Waiting", []frame{
{"runtime.selectgo", 0}, {"runtime.selectgo", 0},
@ -365,7 +366,7 @@ func TestTraceStacks(t *testing.T) {
}}, }},
{trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{ {trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{
{"runtime.selectgo", 0}, {"runtime.selectgo", 0},
{"main.main", mainLine + 86}, {"main.main", mainLine + 91},
}}, }},
{trace.EventStateTransition, "Goroutine Running->Waiting", []frame{ {trace.EventStateTransition, "Goroutine Running->Waiting", []frame{
{"sync.(*Mutex).Lock", 0}, {"sync.(*Mutex).Lock", 0},
@ -382,7 +383,7 @@ func TestTraceStacks(t *testing.T) {
{trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{ {trace.EventStateTransition, "Goroutine Waiting->Runnable", []frame{
{"sync.(*WaitGroup).Add", 0}, {"sync.(*WaitGroup).Add", 0},
{"sync.(*WaitGroup).Done", 0}, {"sync.(*WaitGroup).Done", 0},
{"main.main", mainLine + 91}, {"main.main", mainLine + 96},
}}, }},
{trace.EventStateTransition, "Goroutine Running->Waiting", []frame{ {trace.EventStateTransition, "Goroutine Running->Waiting", []frame{
{"sync.(*Cond).Wait", 0}, {"sync.(*Cond).Wait", 0},
@ -402,6 +403,10 @@ func TestTraceStacks(t *testing.T) {
{"runtime.GOMAXPROCS", 0}, {"runtime.GOMAXPROCS", 0},
{"main.main", 0}, {"main.main", 0},
}}, }},
{trace.EventStateTransition, "Goroutine Running->Runnable", []frame{
{"main.syncPreemptPoint", 0},
{"main.main.func12", 0},
}},
} }
if !stress { if !stress {
// Only check for this stack if !stress because traceAdvance alone could // Only check for this stack if !stress because traceAdvance alone could

View file

@ -3307,10 +3307,10 @@ func execute(gp *g, inheritTime bool) {
tryRecordGoroutineProfile(gp, nil, osyield) tryRecordGoroutineProfile(gp, nil, osyield)
} }
// Assign gp.m before entering _Grunning so running Gs have an // Assign gp.m before entering _Grunning so running Gs have an M.
// M.
mp.curg = gp mp.curg = gp
gp.m = mp gp.m = mp
gp.syncSafePoint = false // Clear the flag, which may have been set by morestack.
casgstatus(gp, _Grunnable, _Grunning) casgstatus(gp, _Grunnable, _Grunning)
gp.waitsince = 0 gp.waitsince = 0
gp.preempt = false gp.preempt = false

View file

@ -466,6 +466,7 @@ type g struct {
runnableTime int64 // the amount of time spent runnable, cleared when running, only used when tracking runnableTime int64 // the amount of time spent runnable, cleared when running, only used when tracking
lockedm muintptr lockedm muintptr
fipsIndicator uint8 fipsIndicator uint8
syncSafePoint bool // set if g is stopped at a synchronous safe point.
runningCleanups atomic.Bool runningCleanups atomic.Bool
sig uint32 sig uint32
writebuf []byte writebuf []byte

View file

@ -1115,6 +1115,9 @@ func newstack() {
shrinkstack(gp) shrinkstack(gp)
} }
// Set a flag indicated that we've been synchronously preempted.
gp.syncSafePoint = true
if gp.preemptStop { if gp.preemptStop {
preemptPark(gp) // never returns preemptPark(gp) // never returns
} }

View file

@ -457,7 +457,7 @@ func (tl traceLocker) GoPreempt() {
// GoStop emits a GoStop event with the provided reason. // GoStop emits a GoStop event with the provided reason.
func (tl traceLocker) GoStop(reason traceGoStopReason) { func (tl traceLocker) GoStop(reason traceGoStopReason) {
tl.eventWriter(tracev2.GoRunning, tracev2.ProcRunning).event(tracev2.EvGoStop, traceArg(trace.goStopReasons[tl.gen%2][reason]), tl.stack(1)) tl.eventWriter(tracev2.GoRunning, tracev2.ProcRunning).event(tracev2.EvGoStop, traceArg(trace.goStopReasons[tl.gen%2][reason]), tl.stack(0))
} }
// GoPark emits a GoBlock event with the provided reason. // GoPark emits a GoBlock event with the provided reason.

View file

@ -109,10 +109,25 @@ func traceStack(skip int, gp *g, gen uintptr) uint64 {
nstk += 1 + fpTracebackPCs(unsafe.Pointer(gp.syscallbp), pcBuf[2:]) nstk += 1 + fpTracebackPCs(unsafe.Pointer(gp.syscallbp), pcBuf[2:])
} else { } else {
pcBuf[1] = gp.sched.pc pcBuf[1] = gp.sched.pc
if gp.syncSafePoint {
// We're stopped in morestack, which is an odd state because gp.sched.bp
// refers to our parent frame, since we haven't had the chance to push our
// frame pointer to the stack yet. If we just start walking from gp.sched.bp,
// we'll skip a frame as a result. Luckily, we can find the PC we want right
// at gp.sched.sp on non-LR platforms, and we have it directly on LR platforms.
// See issue go.dev/issue/68090.
if usesLR {
pcBuf[2] = gp.sched.lr
} else {
pcBuf[2] = *(*uintptr)(unsafe.Pointer(gp.sched.sp))
}
nstk += 2 + fpTracebackPCs(unsafe.Pointer(gp.sched.bp), pcBuf[3:])
} else {
nstk += 1 + fpTracebackPCs(unsafe.Pointer(gp.sched.bp), pcBuf[2:]) nstk += 1 + fpTracebackPCs(unsafe.Pointer(gp.sched.bp), pcBuf[2:])
} }
} }
} }
}
if nstk > 0 { if nstk > 0 {
nstk-- // skip runtime.goexit nstk-- // skip runtime.goexit
} }