Skip to content

Commit 991f6ce

Browse files
committed
runtime/jit: scan precise user frame stack maps
1 parent faa7b7c commit 991f6ce

18 files changed

Lines changed: 832 additions & 289 deletions

api/next/78189.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@ pkg runtime/jit, const UnwindSkip UnwindMode #78189
33
pkg runtime/jit, const UnwindStop UnwindMode #78189
44
pkg runtime/jit, func Register(Region) Handle #78189
55
pkg runtime/jit, method (Handle) Unregister() #78189
6+
pkg runtime/jit, method (Handle) AddStackMaps(...StackMap) #78189
67
pkg runtime/jit, type Handle struct #78189
78
pkg runtime/jit, type Region struct #78189
89
pkg runtime/jit, type Region struct, Describe func(uintptr) (string, string, int, bool) #78189
910
pkg runtime/jit, type Region struct, End uintptr #78189
10-
pkg runtime/jit, type Region struct, Next func(uintptr, uintptr) (uintptr, uintptr, uintptr, bool) #78189
11-
pkg runtime/jit, type Region struct, ScanStack func(func(uintptr)) #78189
11+
pkg runtime/jit, type Region struct, StackMaps []StackMap #78189
1212
pkg runtime/jit, type Region struct, Start uintptr #78189
1313
pkg runtime/jit, type Region struct, Unwind UnwindMode #78189
1414
pkg runtime/jit, func Preempt() bool #78189
15+
pkg runtime/jit, type StackMap struct #78189
16+
pkg runtime/jit, type StackMap struct, CallerBPOffset uintptr #78189
17+
pkg runtime/jit, type StackMap struct, CallerPCOffset uintptr #78189
18+
pkg runtime/jit, type StackMap struct, CallerSPOffset uintptr #78189
19+
pkg runtime/jit, type StackMap struct, FrameOffset uintptr #78189
20+
pkg runtime/jit, type StackMap struct, FrameWords uintptr #78189
21+
pkg runtime/jit, type StackMap struct, HasUnwind bool #78189
22+
pkg runtime/jit, type StackMap struct, PCOffset uintptr #78189
23+
pkg runtime/jit, type StackMap struct, PointerMask []uint8 #78189
24+
pkg runtime/jit, type StackMap struct, UnwindBaseDeltaOffset uintptr #78189
25+
pkg runtime/jit, type StackMap struct, UnwindBaseOffset uintptr #78189
26+
pkg runtime/jit, type StackMap struct, UnwindBaseUsesDelta bool #78189
1527
pkg runtime/jit, type UnwindMode uint8 #78189

src/runtime/jit/jit.go

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@
2424
//
2525
// - [UnwindStop]: The traceback ends at the user frame boundary.
2626
// - [UnwindSkip]: The user frame is skipped and the traceback continues
27-
// past it using the provided Next callback.
27+
// past it using the registered safepoint's unwind recipe.
2828
// - [UnwindDeclare]: Like UnwindSkip, but the user frame is also
2929
// described in tracebacks using the provided Describe callback.
3030
//
3131
// # GC integration
3232
//
33-
// If user code holds Go pointers (e.g., on a shadow stack), the
34-
// ScanStack callback must be provided so the GC can find and mark those
35-
// pointers. Failure to do so may cause the GC to collect live objects.
33+
// If user code holds Go pointers in an active frame, StackMaps must describe
34+
// those slots precisely. The runtime uses the same map both
35+
// while marking the goroutine stack and while relocating a growing stack.
36+
// Failure to describe a live pointer may cause the GC to collect its target
37+
// or leave a stale pointer after stack growth.
3638
package jit
3739

3840
import (
@@ -48,15 +50,46 @@ const (
4850
// UnwindStop ends the traceback at the user frame boundary.
4951
UnwindStop UnwindMode = iota
5052

51-
// UnwindSkip skips user frames and continues unwinding using
52-
// the Next callback. The user frames do not appear in tracebacks.
53+
// UnwindSkip skips user frames and continues unwinding using the
54+
// safepoint metadata. The user frames do not appear in tracebacks.
5355
UnwindSkip
5456

5557
// UnwindDeclare is like UnwindSkip but also describes user frames
5658
// in tracebacks using the Describe callback.
5759
UnwindDeclare
5860
)
5961

62+
// StackMap describes pointer-bearing words in a user frame at one safepoint.
63+
// PointerMask has one bit per frame word in least-significant-bit-first order;
64+
// a set bit denotes a Go pointer. The runtime copies maps passed to Register
65+
// or AddStackMaps, so callers may release or reuse the input afterward.
66+
type StackMap struct {
67+
// PCOffset is the return-PC offset from Region.Start. Entries in a Region
68+
// must be strictly ordered by PCOffset.
69+
PCOffset uintptr
70+
71+
// FrameOffset is added to the user frame SP to locate the first word
72+
// described by PointerMask.
73+
FrameOffset uintptr
74+
75+
// FrameWords is the number of pointer-sized words described by the map.
76+
FrameWords uintptr
77+
78+
PointerMask []byte
79+
80+
// HasUnwind enables the declarative unwind recipe below. base starts at
81+
// sp+UnwindBaseOffset. If UnwindBaseUsesDelta is set, the uintptr stored at
82+
// sp+UnwindBaseDeltaOffset is added to base. CallerPC and CallerBP are read
83+
// from base plus their offsets; CallerSP is base+CallerSPOffset.
84+
HasUnwind bool
85+
UnwindBaseOffset uintptr
86+
UnwindBaseDeltaOffset uintptr
87+
UnwindBaseUsesDelta bool
88+
CallerPCOffset uintptr
89+
CallerSPOffset uintptr
90+
CallerBPOffset uintptr
91+
}
92+
6093
// Region describes a range of executable memory containing user code.
6194
type Region struct {
6295
// Start is the first byte of the code region (inclusive).
@@ -74,33 +107,30 @@ type Region struct {
74107
// UnwindDeclare. Must not allocate Go memory. May be nil.
75108
Describe func(pc uintptr) (name, file string, line int, ok bool)
76109

77-
// Next returns the caller's PC, SP, and BP for a frame at the
78-
// given PC and SP. This allows the runtime to unwind past user
79-
// frames. Must not allocate Go memory.
80-
// Required for UnwindSkip and UnwindDeclare modes.
81-
// May be nil for UnwindStop (traceback simply ends).
82-
//
83-
// The callback runs on the system stack and must not grow the stack.
84-
// It receives:
85-
// pc: the return address pointing into user code
86-
// sp: the frame pointer of the Go callee (= user frame's SP before call)
87-
// It must return:
88-
// callerPC: the return address of the Go caller (above the user frame)
89-
// callerSP: the SP of the Go caller
90-
// callerBP: unused, reserved for future use
91-
Next func(pc, sp uintptr) (callerPC, callerSP, callerBP uintptr, ok bool)
92-
93-
// ScanStack is called during garbage collection to report Go
94-
// pointers held in user stack frames or shadow stacks.
95-
// Must not allocate Go memory. May be nil.
96-
ScanStack func(report func(ptr uintptr))
110+
// StackMaps contains immutable GC and optional unwind metadata for
111+
// safepoints in this region. The runtime performs lookup itself without
112+
// calling user code. Declarative unwind recipes avoid executing instrumented
113+
// Go callbacks from the runtime system stack.
114+
// It may initially be nil when maps are published with Handle.AddStackMaps
115+
// before the corresponding code becomes reachable.
116+
StackMaps []StackMap
97117
}
98118

99119
// Handle represents a registered user frame region.
100120
type Handle struct {
101121
handle uintptr
102122
}
103123

124+
// AddStackMaps appends safepoints to a registered region. Entries must be
125+
// strictly ordered after all previously published entries. The runtime copies
126+
// maps and masks before publishing them to lock-free stack walkers.
127+
//
128+
// JIT compilers must call AddStackMaps after the corresponding machine code is
129+
// complete and before making that code reachable by another goroutine.
130+
func (h Handle) AddStackMaps(stackMaps ...StackMap) {
131+
addUserFrameStackMaps(h.handle, stackMaps)
132+
}
133+
104134
// liveRegions keeps function pointers (closures) alive so the GC does
105135
// not collect them. The runtime stores these pointers in persistentalloc
106136
// memory that is invisible to the GC, so we must retain a reference here.
@@ -115,8 +145,7 @@ func Register(r Region) Handle {
115145
r.Start, r.End,
116146
uint8(r.Unwind),
117147
r.Describe,
118-
r.Next,
119-
r.ScanStack,
148+
r.StackMaps,
120149
)
121150

122151
// Keep closures alive for the GC.
@@ -130,7 +159,10 @@ func Register(r Region) Handle {
130159
return Handle{handle: h}
131160
}
132161

133-
// Unregister removes the user frame region from the runtime.
162+
// Unregister removes the user frame region from the runtime. The caller must
163+
// ensure that no goroutine is executing code in the region, that no user frame
164+
// from the region remains on a goroutine stack, and that the code cannot be
165+
// entered again.
134166
func (h Handle) Unregister() {
135167
unregisterUserFrameRegion(h.handle)
136168

@@ -165,9 +197,11 @@ func userFramePreempt() bool
165197
//go:linkname registerUserFrameRegion runtime/jit.registerUserFrameRegion
166198
func registerUserFrameRegion(start, end uintptr, unwindMode uint8,
167199
describe func(pc uintptr) (string, string, int, bool),
168-
next func(pc, sp uintptr) (uintptr, uintptr, uintptr, bool),
169-
scanStack func(report func(ptr uintptr)),
200+
stackMaps []StackMap,
170201
) uintptr
171202

172203
//go:linkname unregisterUserFrameRegion runtime/jit.unregisterUserFrameRegion
173204
func unregisterUserFrameRegion(handle uintptr)
205+
206+
//go:linkname addUserFrameStackMaps runtime/jit.addUserFrameStackMaps
207+
func addUserFrameStackMaps(handle uintptr, stackMaps []StackMap)

src/runtime/jit/jit_test.go

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,10 @@ func TestPanicThroughUserFrameWithCall(t *testing.T) {
144144
defer freeExecutable(addr, size)
145145

146146
h := jit.Register(jit.Region{
147-
Start: addr,
148-
End: addr + uintptr(size),
149-
Unwind: jit.UnwindSkip,
150-
Next: nextCallback(),
147+
Start: addr,
148+
End: addr + uintptr(size),
149+
Unwind: jit.UnwindSkip,
150+
StackMaps: callTrampolineStackMaps(),
151151
})
152152
defer h.Unregister()
153153

@@ -172,7 +172,7 @@ func TestUnwindStop(t *testing.T) {
172172
}
173173
defer freeExecutable(addr, size)
174174

175-
// Register with UnwindStop no Next callback.
175+
// Register with UnwindStop and no safepoint metadata.
176176
// The panic should be unable to find the defer/recover, so the
177177
// program would crash. We test this in a subprocess.
178178
h := jit.Register(jit.Region{
@@ -182,7 +182,7 @@ func TestUnwindStop(t *testing.T) {
182182
})
183183
defer h.Unregister()
184184

185-
// With UnwindStop and no Next callback, the unwinder stops at the
185+
// With UnwindStop and no unwind recipe, the unwinder stops at the
186186
// JIT boundary. The panic cannot reach callAndRecover's defer, so
187187
// recover() returns nil and the panic continues to crash.
188188
// We just verify the registration itself doesn't crash.
@@ -290,10 +290,10 @@ func TestPanicRecoverMultipleGoroutines(t *testing.T) {
290290
defer freeExecutable(addr, size)
291291

292292
h := jit.Register(jit.Region{
293-
Start: addr,
294-
End: addr + uintptr(size),
295-
Unwind: jit.UnwindSkip,
296-
Next: nextCallback(),
293+
Start: addr,
294+
End: addr + uintptr(size),
295+
Unwind: jit.UnwindSkip,
296+
StackMaps: callTrampolineStackMaps(),
297297
})
298298
defer h.Unregister()
299299

@@ -336,7 +336,7 @@ func TestUnwindDeclareDescribe(t *testing.T) {
336336
atomic.AddUint32(&describeCalled, 1)
337337
return "myJitFunction", "jit_generated.go", 42, true
338338
},
339-
Next: nextCallback(),
339+
StackMaps: callTrampolineStackMaps(),
340340
})
341341
defer h.Unregister()
342342

@@ -376,7 +376,7 @@ func TestUnwindDeclareInStackTrace(t *testing.T) {
376376
Describe: func(pc uintptr) (string, string, int, bool) {
377377
return "myJitFunction", "jit_generated.go", 42, true
378378
},
379-
Next: nextCallback(),
379+
StackMaps: callTrampolineStackMaps(),
380380
})
381381
defer h.Unregister()
382382

@@ -395,33 +395,25 @@ func TestUnwindDeclareInStackTrace(t *testing.T) {
395395
}
396396
}
397397

398-
// TestScanStackGC tests that the ScanStack callback is invoked during
399-
// garbage collection when registered.
400-
func TestScanStackGC(t *testing.T) {
398+
// TestStackMapInactive verifies that registering stack maps without an active
399+
// frame does not add global GC roots.
400+
func TestStackMapInactive(t *testing.T) {
401401
code := retTrampoline()
402402
addr, size, err := allocExecutable(code)
403403
if err != nil {
404404
t.Fatalf("allocExecutable: %v", err)
405405
}
406406
defer freeExecutable(addr, size)
407407

408-
var scanCalled uint32
409408
h := jit.Register(jit.Region{
410-
Start: addr,
411-
End: addr + uintptr(size),
412-
Unwind: jit.UnwindStop,
413-
ScanStack: func(report func(ptr uintptr)) {
414-
atomic.AddUint32(&scanCalled, 1)
415-
},
409+
Start: addr,
410+
End: addr + uintptr(size),
411+
Unwind: jit.UnwindStop,
412+
StackMaps: []jit.StackMap{{PCOffset: 0, FrameWords: 1, PointerMask: []byte{1}}},
416413
})
417414
defer h.Unregister()
418415

419-
// Force GC. The ScanStack callback should be invoked.
420416
runtime.GC()
421-
422-
if atomic.LoadUint32(&scanCalled) == 0 {
423-
t.Fatal("ScanStack callback was not called during GC")
424-
}
425417
}
426418

427419
// TestPreemptReturnsFalseNormally tests that Preempt returns false when

0 commit comments

Comments
 (0)