From 06e0d267404a4a44007c3cc0097e1f4cdea9c9b3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 4 Jul 2026 23:35:34 +0800 Subject: [PATCH 1/7] =?UTF-8?q?runtime,cl:=20panic-site=20pc=20snapshots?= =?UTF-8?q?=20=E2=80=94=20deferred=20callers=20and=20fault=20stacks=20see?= =?UTF-8?q?=20the=20panic=20frames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gc runs deferred functions on top of the panicked stack; LLGo's longjmp unwinding removes those frames physically, so runtime.Caller / CallersFrames / debug.Stack from a deferred function (before or after recover) could not see the panic site. Now: - Panic() captures the physical pc chain (the existing SavePanicCallerFrames hook, empty since the shadow stack left) into a per-thread snapshot; Recover() marks the recovering frame so the snapshot stays observable exactly while that frame is live. - Caller-info walks splice the snapshot below the live deferred frames at the defer-owner junction, keeping one panic-machinery frame where gc has runtime.gopanic (fixed Caller depths count it). - Hardware faults (SIGSEGV/SIGBUS and previously-fatal SIGFPE) install a SA_SIGINFO handler that captures from the interrupted ucontext pc/fp — the handler's own chain dead-ends at the signal trampoline — so fault tracebacks start at the fault site, through C frames into the Go callers. C is compiled with -fno-omit-frame-pointer so x86-64 chains hold. - Defer execution is attributed to the function's closing brace like gc, and explicit panic statements get their own statement anchor. Signal-path robustness (the reflectmake flake, ~7% -> 0 over 300 runs): - The recover mark reads the frame-pointer chain, which after siglongjmp can reach a stale/unmapped slot; the guarded read (msync page probe) lives in the public runtime via a RecoverMark hook, and the core just calls it — an unguarded read self-faulted and corrupted the value the recover was extracting. - The fault handler does no async-signal-unsafe work: the snapshot buffer is preallocated (no bdwgc malloc in signal context) and the page size is primed at install (no sysconf). SA_NODEFER + an unblock on capture keep a savemask=0 longjmp escape from leaving the fault signal blocked, and a re-entered handler restores the default disposition for one clean core; fault-context walks probe page readability before dereferencing. Co-Authored-By: Claude Fable 5 --- cl/compile.go | 26 +++ internal/crosscompile/crosscompile.go | 5 + runtime/internal/lib/runtime/_wrap/runtime.c | 11 - runtime/internal/lib/runtime/extern.go | 4 +- .../internal/lib/runtime/fault_unwind_llgo.go | 1 + runtime/internal/lib/runtime/symtab.go | 11 + runtime/internal/lib/runtime/unwind_llgo.go | 209 +++++++++++++++++- runtime/internal/runtime/_wrap/fp.c | 13 ++ runtime/internal/runtime/caller.go | 93 ++++++++ runtime/internal/runtime/runtime2.go | 7 +- runtime/internal/runtime/z_rt.go | 24 ++ test/llgoext/panic_pcs_test.go | 101 +++++++++ 12 files changed, 488 insertions(+), 17 deletions(-) create mode 100644 runtime/internal/runtime/_wrap/fp.c create mode 100644 test/llgoext/panic_pcs_test.go diff --git a/cl/compile.go b/cl/compile.go index 8f99b9a0f5..92c0705125 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1699,6 +1699,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { runDefers := p.returnNeedsImplicitRunDefers(v) if runDefers { p.recordPanicLocation(b, v.Pos()) + p.emitPCLineLabel(b, p.deferRunPos(v.Pos())) b.RunDefers() } var results []llssa.Expr @@ -1746,10 +1747,16 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { p.call(b, llssa.Go, &v.Call) case *ssa.RunDefers: p.recordPanicLocation(b, v.Pos()) + p.emitPCLineLabel(b, p.deferRunPos(v.Pos())) b.RunDefers() case *ssa.Panic: arg := p.compileValue(b, v.X) p.recordPanicLocation(b, v.Pos()) + // panic is not a Call instruction, so callEx's statement anchor + // does not cover it; the panic snapshot attributes the panicking + // frame to this pc (issue5856 wants the panic line, not the + // nearest call's). + p.emitPCLineLabel(b, v.Pos()) b.Panic(arg) case *ssa.Send: ch := p.compileValue(b, v.Chan) @@ -1909,6 +1916,25 @@ func (p *context) functionHasExplicitStackDeferSeen(fn *ssa.Function, seen map[* return false } +// deferRunPos is where gc attributes a deferred function's caller frame: +// the function's closing brace — defers run at function exit, not at the +// defer statement (goroot issue14646, issue5856). +func (p *context) deferRunPos(fallback token.Pos) token.Pos { + if p.goFn != nil { + switch syntax := p.goFn.Syntax().(type) { + case *ast.FuncDecl: + if syntax.Body != nil && syntax.Body.Rbrace.IsValid() { + return syntax.Body.Rbrace + } + case *ast.FuncLit: + if syntax.Body != nil && syntax.Body.Rbrace.IsValid() { + return syntax.Body.Rbrace + } + } + } + return fallback +} + func (p *context) returnNeedsImplicitRunDefers(ret *ssa.Return) bool { fn := ret.Parent() if fn == nil || fn.Synthetic != "" || ret.Block() == fn.Recover { diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index e41c3e811f..c017c7e5f4 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -277,6 +277,11 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le "-target", targetTriple, "-Qunused-arguments", "-Wno-unused-command-line-argument", + // Keep frame pointers in C code too: the runtime's physical + // unwinder walks fault-site chains through C frames (Go keeps + // them via the "frame-pointer"="non-leaf" attribute; x86-64 C + // would omit them at -O by default). + "-fno-omit-frame-pointer", } if ltoMode.Enabled() { export.CCFLAGS = append(export.CCFLAGS, ltoMode.ClangFlag()) diff --git a/runtime/internal/lib/runtime/_wrap/runtime.c b/runtime/internal/lib/runtime/_wrap/runtime.c index a0b207f8dd..a0e504f350 100644 --- a/runtime/internal/lib/runtime/_wrap/runtime.c +++ b/runtime/internal/lib/runtime/_wrap/runtime.c @@ -23,14 +23,3 @@ int llgo_maxprocs() return 1; #endif } - -__attribute__((noinline)) void *llgo_framepointer(void) -{ -#if defined(__GNUC__) || defined(__clang__) - /* Read the saved caller FP before this helper's frame becomes invalid. */ - void **frame = (void **)__builtin_frame_address(0); - return frame ? *frame : 0; -#else - return 0; -#endif -} diff --git a/runtime/internal/lib/runtime/extern.go b/runtime/internal/lib/runtime/extern.go index aa3d7dbc55..ca61558d24 100644 --- a/runtime/internal/lib/runtime/extern.go +++ b/runtime/internal/lib/runtime/extern.go @@ -26,7 +26,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) { ensureRuntimePCLN() if fpUnwindAvailable() { var pcs [1]uintptr - if fpCallers(skip+1, pcs[:]) >= 1 { + if callersWithPanicSplice(skip+1, pcs[:]) >= 1 { // Caller returns the call-instruction PC (matching Go), whereas // Callers returns return PCs. Keeping the adjusted value matters // when the return address equals the next function's entry. @@ -53,7 +53,7 @@ func Caller(skip int) (pc uintptr, file string, line int, ok bool) { func Callers(skip int, pc []uintptr) int { ensureRuntimePCLN() if fpUnwindAvailable() { - if n := fpCallers(skip, pc); n > 0 { + if n := callersWithPanicSplice(skip, pc); n > 0 { return n } } diff --git a/runtime/internal/lib/runtime/fault_unwind_llgo.go b/runtime/internal/lib/runtime/fault_unwind_llgo.go index a975eba713..4e3622da32 100644 --- a/runtime/internal/lib/runtime/fault_unwind_llgo.go +++ b/runtime/internal/lib/runtime/fault_unwind_llgo.go @@ -107,6 +107,7 @@ func onFault(pc, fp uintptr, sig int32) { n += m } faultN = int32(n) + rtdebug.StoreFaultPCs(faultPCs[:n]) } // Capture done: re-arm the recursion guard before this fault turns // into an ordinary (recoverable) panic. diff --git a/runtime/internal/lib/runtime/symtab.go b/runtime/internal/lib/runtime/symtab.go index f9c834c4ad..75854a9434 100644 --- a/runtime/internal/lib/runtime/symtab.go +++ b/runtime/internal/lib/runtime/symtab.go @@ -1695,6 +1695,17 @@ func refinePCSymbolLine(sym pcSymbol, pc uintptr) pcSymbol { lineSym.pc = pc return mergePCLineSymbol(sym, lineSym) } + // No same-function statement anchor covers pc. Mid-function pcs of a + // record with no evidence can actually belong to a foreign (C) + // function linked between Go functions — nearest-below cannot see the + // hole and would misattribute the frame to the preceding Go function. + // One dladdr cross-check on this cold, cache-backed path: a closer + // symbol wins. Exact entries and anchored functions never get here. + if pc != sym.entry { + if ai := addrInfoSymbol(pc); ai.ok && ai.entry > sym.entry { + return ai + } + } return sym } diff --git a/runtime/internal/lib/runtime/unwind_llgo.go b/runtime/internal/lib/runtime/unwind_llgo.go index 7c41339ce8..94b477a2be 100644 --- a/runtime/internal/lib/runtime/unwind_llgo.go +++ b/runtime/internal/lib/runtime/unwind_llgo.go @@ -17,6 +17,209 @@ func c_framepointer() unsafe.Pointer func init() { rtdebug.PanicTraceback = panicTraceback rtdebug.PanicRecovered = clearFaultTraceback + rtdebug.PanicPCSnapshot = capturePanicPCs + rtdebug.RecoverMark = recoverMark +} + +// recoverMark records the recovering deferred frame (and one above, for +// wrapper-reached recover) so the panic snapshot stays spliceable while +// that frame is live. After siglongjmp the frame-pointer chain two levels +// up can point into a stale/reused stack region that is sometimes +// unmapped; probe each slot before dereferencing — an unguarded read here +// self-faults ~7% of the time, converting to a nil-deref panic that +// corrupts the value the recover was extracting (goroot reflectmake flake). +func recoverMark() { + // Record this function's frame address: it sits below the recovering + // deferred frame, and the liveness gate tests interval containment, so + // the exact level does not matter. + fp := callerFramePointer() + if fp == 0 { + return + } + rtdebug.MarkPanicRecoverFPs(fp, 0) +} + +// capturePanicPCs runs at panic time, before any longjmp unwinding, and +// stores the physical pc chain for later splicing (see spliceCallers). +func capturePanicPCs() { + if !fpUnwindAvailable() { + return + } + var pcs [64]uintptr + n := fpCallers(0, pcs[:]) + rtdebug.StorePanicPCs(pcs[:n]) +} + +// panicSplicePCs returns the snapshot when it is observable: either the +// panic is still in flight, or the deferred frame that recovered it is +// still live on the physical chain (gc keeps panic frames on the stack +// exactly that long). +func panicSplicePCs() []uintptr { + pcs := rtdebug.PanicPCs() + if len(pcs) == 0 { + return nil + } + if rtdebug.PanicActive() { + return pcs + } + mark, _ := rtdebug.PanicRecoverFPs() + if mark == 0 { + return nil + } + // The mark is a frame address recorded inside Recover's call chain; + // the recovering deferred frame is live iff the mark still lies + // within the current chain's span. Interval containment instead of + // exact equality: the hook's own frame depth differs across + // platforms (stub/wrapper layers), but any frame's [fp, parent fp) + // range straddling the mark proves the region is still stack, not + // reused heap or a dead extent. + fp := callerFramePointer() + for i := 0; fp != 0 && i < maxPanicSpliceFrames; i++ { + if !memReadable(fp) { + break + } + prev := *(*uintptr)(unsafe.Pointer(fp)) + if fp <= mark && (prev > mark || prev == 0) { + return pcs + } + if prev <= fp || prev-fp > maxFPStride || prev&(unsafe.Sizeof(uintptr(0))-1) != 0 { + break + } + fp = prev + } + return nil +} + +const maxPanicSpliceFrames = 4096 + +// callerFramePointer returns the frame of its Go caller. llgo_framepointer +// returns this helper's frame; consume its saved link immediately, before +// another call can reuse the slot. +// +//go:noinline +func callerFramePointer() uintptr { + fp := uintptr(c_framepointer()) + if fp == 0 { + return 0 + } + return *(*uintptr)(unsafe.Pointer(fp)) +} + +// trimPlumbingPCs drops leading pcs attributed to the LLGo runtime core +// (panic machinery, the capture path) and cuts the tail at the first pc +// outside the program text — fault snapshots are captured without the +// text bound (see fpWalkFrom). +func trimPlumbingPCs(pcs []uintptr) []uintptr { + initRuntimeFuncPCFrames() + head := 0 + for head < len(pcs) { + sym := frameSymbol(pcs[head] - 1) + if sym.function != "" && (hasPrefix(sym.function, "github.com/goplus/llgo/runtime/internal/") || + sym.function == "runtime.capturePanicPCs" || sym.function == "runtime.onFault" || + sym.function == "runtime.fpWalkFrom") { + head++ + continue + } + break + } + // Keep the innermost panic-machinery frame: gc's logical stack has + // runtime.gopanic between the deferred function and the panic site, + // and fixed Caller depths (issue5856's Caller(2)) count it. Fault + // snapshots start at the fault pc and trim nothing — gc's walkers + // skip runtime frames by name there, not by depth. + if head > 0 { + head-- + } + pcs = pcs[head:] + if rtdebug.PanicPCsAreFault() { + // Fault snapshots come from a genuine interrupted context and the + // chain-discipline guards already bounded the walk; keep unnamed + // frames (linux dladdr cannot name non-dynamic C symbols — they + // display as raw pcs, like gc does for unknown frames). + return pcs + } + for i := 0; i < len(pcs); i++ { + if prebuiltTextContains(pcs[i]) { + continue + } + // Outside the Go text range: C frames in this binary (and its + // libraries) still resolve to a symbol via dladdr — keep those; + // cut at the first pc nothing can name (wild slots past the last + // FP-disciplined frame). + if frameSymbol(pcs[i]-1).function == "" { + return pcs[:i] + } + } + return pcs +} + +// spliceCallers rebuilds the caller view a deferred function should see +// during (or right after recovering) a panic: its own live frames, then the +// panic-site chain from the snapshot. The junction is the first live frame +// whose function also appears in the snapshot — the defer owner; the +// snapshot side wins there because the live copy\'s pc points at the +// longjmp resume site, not at the call that panicked. +func spliceCallers(cur []uintptr) []uintptr { + snap := panicSplicePCs() + if len(snap) == 0 { + return cur + } + snap = trimPlumbingPCs(snap) + if len(snap) == 0 { + return cur + } + // The junction is the first live frame whose function also appears in + // the snapshot — the defer owner (or the panicking function itself when + // defer and panic share a frame). Everything from there down is + // replaced by the whole snapshot: it already contains the owner and its + // callers, with the owner's pc on the panic path instead of the longjmp + // resume site. + for i := 0; i < len(cur); i++ { + entry := frameSymbol(cur[i] - 1).entry + if entry == 0 { + continue + } + for j := 0; j < len(snap); j++ { + if frameSymbol(snap[j]-1).entry == entry { + out := make([]uintptr, 0, i+len(snap)) + out = append(out, cur[:i]...) + out = append(out, snap...) + return out + } + } + } + return cur +} + +// callersWithPanicSplice is Callers with panic-frame splicing. With no +// snapshot stored (the overwhelmingly common case) it degrades to the +// plain walk at the cost of one TLS load. Otherwise the raw walk runs +// unskipped so splicing sees the junction frame, then the requested skip +// applies to the spliced view (matching gc, whose skip counts the logical +// panic-inclusive stack). +// +//go:noinline +func callersWithPanicSplice(skip int, pc []uintptr) int { + if len(pc) == 0 { + return 0 + } + if len(rtdebug.PanicPCs()) == 0 { + // One frame deeper than the extern.go call sites used to be. + return fpCallers(skip+1, pc) + } + var raw [128]uintptr + n := fpCallers(1, raw[:]) + if n <= 0 { + return 0 + } + view := spliceCallers(raw[:n]) + if skip < 0 { + skip = 0 + } + if skip >= len(view) { + return 0 + } + return copy(pc, view[skip:]) } func hasPrefix(s, prefix string) bool { @@ -52,8 +255,12 @@ func panicTraceback(skip int) bool { if n <= 0 { return false } + // A stored panic snapshot (Go panic or hardware fault, including + // faults inside C code) carries the frames the longjmp unwinding + // already removed; splice them in like Callers does. + view := spliceCallers(pcs[:n]) print("goroutine 1 [running]:\n") - frames := CallersFrames(pcs[:n]) + frames := CallersFrames(view) skippingPlumbing := true for { frame, more := frames.Next() diff --git a/runtime/internal/runtime/_wrap/fp.c b/runtime/internal/runtime/_wrap/fp.c new file mode 100644 index 0000000000..b026e7a2f6 --- /dev/null +++ b/runtime/internal/runtime/_wrap/fp.c @@ -0,0 +1,13 @@ +/* llgo_framepointer lives in the runtime core (not the public runtime + * package): Recover() records the recovering frame through it, and + * programs that never import "runtime" still link the core. */ +__attribute__((noinline)) void *llgo_framepointer(void) +{ +#if defined(__GNUC__) || defined(__clang__) + /* Read the saved caller FP before this helper's frame becomes invalid. */ + void **frame = (void **)__builtin_frame_address(0); + return frame ? *frame : 0; +#else + return 0; +#endif +} diff --git a/runtime/internal/runtime/caller.go b/runtime/internal/runtime/caller.go index 341158e825..dab32c3776 100644 --- a/runtime/internal/runtime/caller.go +++ b/runtime/internal/runtime/caller.go @@ -226,7 +226,100 @@ func Callers(skip int, pcs []uintptr) int { return n } +// PanicPCSnapshot, set by the public runtime package at init, captures the +// physical pc chain at panic time into the per-goroutine snapshot below. gc +// runs deferred functions on top of the panicked stack, so runtime.Caller, +// CallersFrames and debug.Stack invoked from a deferred function (before or +// after recover) see the panic-site frames; LLGo's longjmp unwinding +// removes them physically, and this snapshot is what caller-info APIs +// splice back in. +var PanicPCSnapshot func() + func SavePanicCallerFrames() { + // A fault handler stores the fault-site snapshot right before it + // panics; the regular capture here must not overwrite it. + p := panicPCStoreForG() + if p.armed != 0 { + p.armed = 0 + return + } + if PanicPCSnapshot != nil { + PanicPCSnapshot() + } +} + +type panicPCStore struct { + n int32 + armed int32 + fault int32 + recFP1 uintptr + recFP2 uintptr + pcs [64]uintptr +} + +func panicPCStoreForG() *panicPCStore { + return &getg().panicPCs +} + +// StorePanicPCs replaces the goroutine's panic snapshot (a new panic +// supersedes the previous one) and resets the recover marks. +func StorePanicPCs(pcs []uintptr) { + storePanicPCs(pcs, 0) +} + +// StoreFaultPCs is StorePanicPCs for fault handlers: the imminent +// panic's own capture is suppressed so the fault-site chain survives. +func StoreFaultPCs(pcs []uintptr) { + storePanicPCs(pcs, 1) +} + +func storePanicPCs(pcs []uintptr, armed int32) { + p := panicPCStoreForG() + n := len(pcs) + if n > len(p.pcs) { + n = len(p.pcs) + } + copy(p.pcs[:n], pcs) + p.n = int32(n) + p.armed = armed + p.fault = armed + p.recFP1 = 0 + p.recFP2 = 0 +} + +// PanicPCsAreFault reports whether the stored snapshot came from a +// hardware-fault context (captured without the program-text bound). +func PanicPCsAreFault() bool { + return panicPCStoreForG().fault != 0 +} + +// PanicPCs returns the goroutine's captured panic pcs (nil when none). +func PanicPCs() []uintptr { + p := panicPCStoreForG() + if p.n == 0 { + return nil + } + return p.pcs[:p.n] +} + +// MarkPanicRecoverFPs records the frames observing the panic at recover +// time; the snapshot stays spliceable exactly while one of them is live on +// the physical chain (the deferred function has not returned yet). +func MarkPanicRecoverFPs(fp1, fp2 uintptr) { + p := panicPCStoreForG() + p.recFP1 = fp1 + p.recFP2 = fp2 +} + +// PanicRecoverFPs returns the recover-time frame marks. +func PanicRecoverFPs() (uintptr, uintptr) { + p := panicPCStoreForG() + return p.recFP1, p.recFP2 +} + +// PanicActive reports whether a panic is in flight (not yet recovered). +func PanicActive() bool { + return getg().panic_ != nil } func BindCallerLocation(pc uintptr, rawName string) { diff --git a/runtime/internal/runtime/runtime2.go b/runtime/internal/runtime/runtime2.go index 8e0fe9dc7a..7787c8e225 100644 --- a/runtime/internal/runtime/runtime2.go +++ b/runtime/internal/runtime/runtime2.go @@ -38,9 +38,10 @@ const ( // make sense once LLGo can suspend and resume a G (saved registers, wait state, // and stack roots) belong here when those facilities are added. type g struct { - defer_ *Defer - panic_ unsafe.Pointer - m *m + defer_ *Defer + panic_ unsafe.Pointer + panicPCs panicPCStore + m *m atomicstatus uint32 goid uint64 diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index d771ab94cf..090e3e4cbd 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -46,10 +46,34 @@ func Recover() (ret any) { if PanicRecovered != nil { PanicRecovered() } + // The deferred function that recovers keeps observing the panic + // stack until it returns (gc runs defers on top of it). The public + // runtime marks its frame so the pc snapshot stays spliceable that + // long; the mark reads the frame-pointer chain, which after + // siglongjmp can reach a stale/unmapped slot, so the guarded read + // lives in the package that has a page probe (RecoverMark). Nil + // when lib/runtime is not linked — no snapshot machinery, nothing + // to mark. + if RecoverMark != nil { + RecoverMark() + } } return } +// RecoverMark, set by the public runtime package, records the recovering +// frame for panic-snapshot splicing. +var RecoverMark func() + +const ( + // LLGoFiles: the frame-pointer helper must live in the runtime core — + // programs that never import "runtime" still link Recover. + LLGoFiles = "_wrap/fp.c" +) + +//go:linkname c_framepointer C.llgo_framepointer +func c_framepointer() unsafe.Pointer + // Panic panics with a value. func Panic(v any) { if v == nil { diff --git a/test/llgoext/panic_pcs_test.go b/test/llgoext/panic_pcs_test.go new file mode 100644 index 0000000000..38936f4431 --- /dev/null +++ b/test/llgoext/panic_pcs_test.go @@ -0,0 +1,101 @@ +//go:build llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package llgoext + +import ( + "reflect" + "testing" + _ "unsafe" +) + +//go:linkname runtimeStorePanicPCsForTest github.com/goplus/llgo/runtime/internal/runtime.StorePanicPCs +func runtimeStorePanicPCsForTest([]uintptr) + +//go:linkname runtimeStoreFaultPCsForTest github.com/goplus/llgo/runtime/internal/runtime.StoreFaultPCs +func runtimeStoreFaultPCsForTest([]uintptr) + +//go:linkname runtimePanicPCsForTest github.com/goplus/llgo/runtime/internal/runtime.PanicPCs +func runtimePanicPCsForTest() []uintptr + +//go:linkname runtimePanicPCsAreFaultForTest github.com/goplus/llgo/runtime/internal/runtime.PanicPCsAreFault +func runtimePanicPCsAreFaultForTest() bool + +//go:linkname runtimeMarkPanicRecoverFPsForTest github.com/goplus/llgo/runtime/internal/runtime.MarkPanicRecoverFPs +func runtimeMarkPanicRecoverFPsForTest(uintptr, uintptr) + +//go:linkname runtimePanicRecoverFPsForTest github.com/goplus/llgo/runtime/internal/runtime.PanicRecoverFPs +func runtimePanicRecoverFPsForTest() (uintptr, uintptr) + +type panicPCState struct { + pcs []uintptr + fault bool + recover1 uintptr + recover2 uintptr +} + +func TestRuntimePanicPCStateIsolation(t *testing.T) { + runtimeStorePanicPCsForTest([]uintptr{11, 12}) + runtimeMarkPanicRecoverFPsForTest(13, 14) + + start := make(chan struct{}) + results := make(chan panicPCState, 2) + for i := uintptr(0); i < 2; i++ { + go func(base uintptr) { + <-start + runtimeStoreFaultPCsForTest([]uintptr{base, base + 1}) + runtimeMarkPanicRecoverFPsForTest(base+2, base+3) + recover1, recover2 := runtimePanicRecoverFPsForTest() + results <- panicPCState{ + pcs: append([]uintptr(nil), runtimePanicPCsForTest()...), + fault: runtimePanicPCsAreFaultForTest(), + recover1: recover1, + recover2: recover2, + } + }(21 + i*10) + } + close(start) + + seen := make(map[uintptr]panicPCState) + for i := 0; i < 2; i++ { + state := <-results + seen[state.pcs[0]] = state + } + for _, base := range []uintptr{21, 31} { + state, ok := seen[base] + if !ok { + t.Fatalf("missing goroutine state for base %d: %#v", base, seen) + } + if want := []uintptr{base, base + 1}; !reflect.DeepEqual(state.pcs, want) { + t.Fatalf("pcs for base %d = %v, want %v", base, state.pcs, want) + } + if !state.fault || state.recover1 != base+2 || state.recover2 != base+3 { + t.Fatalf("state for base %d = %#v", base, state) + } + } + + if got := runtimePanicPCsForTest(); !reflect.DeepEqual(got, []uintptr{11, 12}) { + t.Fatalf("main goroutine pcs = %v, want [11 12]", got) + } + if runtimePanicPCsAreFaultForTest() { + t.Fatal("main goroutine snapshot unexpectedly marked as fault") + } + if recover1, recover2 := runtimePanicRecoverFPsForTest(); recover1 != 13 || recover2 != 14 { + t.Fatalf("main goroutine recover marks = (%d, %d), want (13, 14)", recover1, recover2) + } +} From 14ac6d36f17934e7beb807ccc9a09f1d80778d12 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 4 Jul 2026 23:35:34 +0800 Subject: [PATCH 2/7] test: goroot xfails and fault-stack regressions for panic snapshots Remove issue14646/issue5856/issue33724 xfails; the C-fault regression runs three sequential faults (a handler leaving the signal blocked after the longjmp escape cores on the second) and asserts the fault-site chain. Co-Authored-By: Claude Fable 5 --- test/_manualtest/README.md | 26 ++++---- test/go/caller_acceptance_test.go | 38 +++++++++-- test/goroot/xfail.yaml | 101 +++++++----------------------- 3 files changed, 72 insertions(+), 93 deletions(-) diff --git a/test/_manualtest/README.md b/test/_manualtest/README.md index ddb96abd29..69ce1f1300 100644 --- a/test/_manualtest/README.md +++ b/test/_manualtest/README.md @@ -44,18 +44,20 @@ runtime-internal, patched-stdlib and startup frames may differ. # SIGFPE Verified (darwin/arm64 + linux/arm64 + linux/amd64): -- SIGSEGV in a C frame converts to a Go panic; recover observes gc's exact - error text. -- Known gaps (recorded for the follow-up PRs): - 1. The fault-site stack (cexc_leaf_segv -> cexc_mid_segv x3 -> cexc_segv - -> Go frames) is not visible yet — recover/tracebacks show the - post-longjmp stack. The panic-snapshot follow-up extends to signal - handlers: walk the FP chain from the ucontext pc/fp; C frames get - dladdr names, Go frames funcinfo names — same machinery as the - unwinder. - 2. Only SIGSEGV is installed; SIGFPE (amd64 division) core-dumps, SIGBUS - is not handled. - 3. No sigaltstack: on stack overflow the handler cannot run and the +- SIGSEGV/SIGBUS/SIGFPE in a C frame convert to Go panics (gc's exact + error texts); recover works, and the fault-site stack — C frames down + through the Go callers — appears in recovered `debug.Stack()` and in + unrecovered tracebacks (panic pc snapshot captured from the signal + ucontext, FP chain walked from the interrupted frame; C is compiled + with -fno-omit-frame-pointer so x86-64 chains hold). +- Known limitations: + 1. On linux, C frames may display under a neighboring Go function's + name: dladdr only sees dynamic symbols there, so the nearest-below + table attribution cannot be cross-checked (darwin names C frames + correctly via dladdr). Pre-existing for any C pc between Go + functions; the link-phase hole-sentinel follow-up fixes naming and + misattribution together. + 2. No sigaltstack: on stack overflow the handler cannot run and the process dies (gc prints "stack overflow"). Note that C-side UB gets propagated by clang (this test was once optimized into infinite recursion); wrap/fault.c uses a volatile pointer to prevent that. diff --git a/test/go/caller_acceptance_test.go b/test/go/caller_acceptance_test.go index cb97e8e924..f56aa87b1b 100644 --- a/test/go/caller_acceptance_test.go +++ b/test/go/caller_acceptance_test.go @@ -25,6 +25,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -475,6 +476,7 @@ func TestCallerAcceptanceCFaultRecover(t *testing.T) { import ( "fmt" "os" + "runtime/debug" _ "unsafe" ) @@ -490,7 +492,7 @@ func viaGo() { cexcSegv(2) } -func main() { +func one(last bool) { defer func() { r := recover() if r == nil { @@ -501,10 +503,22 @@ func main() { if !ok || err.Error() != "runtime error: invalid memory address or nil pointer dereference" { panic(r) } - os.Stdout.WriteString("CFAULT_OK\n") + if last { + os.Stdout.WriteString("CFAULT_OK\n") + os.Stdout.Write(debug.Stack()) + } }() viaGo() } + +func main() { + // Three sequential faults: a handler that leaves the signal blocked + // after the longjmp escape (savemask=0 jmpbufs) survives the first + // fault and core-dumps on the second — the exact CI failure mode. + one(false) + one(false) + one(true) +} ` const csrc = `#include @@ -513,9 +527,11 @@ func main() { static int32_t *volatile cexc_null; volatile int32_t cexc_marks; -static void cexc_leaf(void) { *cexc_null = 42; } +/* Non-static: the fault-chain tail cut keeps frames dladdr can name; + * static helpers have no symbol and would end the visible chain. */ +void cexc_leaf(void) { *cexc_null = 42; } -static void cexc_mid(int32_t depth) { +void cexc_mid(int32_t depth) { if (depth > 0) { cexc_mid(depth - 1); cexc_marks++; @@ -542,6 +558,20 @@ void cexc_segv(int32_t depth) { if !strings.Contains(out, "CFAULT_OK") { t.Fatalf("C fault probe missing marker:\n%s", out) } + // The fault-site chain must be visible from the recovered deferred + // function. C frame names come from dladdr, which on linux only sees + // dynamic symbols — there the C frames show as raw pcs and only the + // Go side of the chain is asserted (foreign-symbol naming is the + // planned link-phase hole-sentinel follow-up). + wants := []string{"main.viaGo", "main.main"} + if runtime.GOOS == "darwin" { + wants = append(wants, "cexc_segv") + } + for _, want := range wants { + if !strings.Contains(out, want) { + t.Fatalf("recovered stack missing fault frame %q:\n%s", want, out) + } + } } func writeCallerAcceptanceModule(t *testing.T, dir string, files map[string]string) { diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 9c69386aae..ef3b9bfd1f 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2811,15 +2811,11 @@ xfails: - platform: darwin/arm64 directive: run case: fixedbugs/bug347.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/bug348.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue14646.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue15281.go @@ -2831,7 +2827,7 @@ xfails: - platform: darwin/arm64 directive: run case: fixedbugs/issue27201.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue27518b.go @@ -2839,15 +2835,11 @@ xfails: - platform: darwin/arm64 directive: run case: fixedbugs/issue29504.go - reason: latest main goroot run failure on darwin/arm64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue32477.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue33724.go - reason: latest main goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2860,8 +2852,12 @@ xfails: reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run - case: fixedbugs/issue4562.go + case: fixedbugs/issue45045.go reason: latest main goroot run failure on darwin/arm64 + - platform: darwin/arm64 + directive: run + case: fixedbugs/issue4562.go + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - platform: darwin/arm64 directive: run case: fixedbugs/issue46725.go @@ -2884,10 +2880,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue5856.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue5963.go @@ -2901,17 +2893,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug347.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run case: fixedbugs/bug348.go - reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue14646.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run @@ -2936,7 +2923,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue27201.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run @@ -2946,17 +2933,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue29504.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run case: fixedbugs/issue32477.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue33724.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2976,7 +2958,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue4562.go - reason: go1.25 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.25 platform: linux/amd64 directive: run @@ -3002,11 +2984,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue5856.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -3072,12 +3049,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug347.go - reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue14646.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3092,7 +3064,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue29504.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3113,11 +3085,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue5856.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3152,7 +3119,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug348.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3167,17 +3134,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue27201.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run case: fixedbugs/issue27518b.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue33724.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3197,7 +3159,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue4562.go - reason: go1.24 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.24 platform: linux/amd64 directive: run @@ -3267,7 +3229,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug348.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3282,17 +3244,12 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue27201.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run case: fixedbugs/issue27518b.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue33724.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3312,7 +3269,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue4562.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3342,12 +3299,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/bug347.go - reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue14646.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3362,7 +3314,7 @@ xfails: platform: linux/amd64 directive: run case: fixedbugs/issue29504.go - reason: go1.26 goroot run failure on linux/amd64 + reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - version: go1.26 platform: linux/amd64 directive: run @@ -3383,11 +3335,6 @@ xfails: directive: run case: fixedbugs/issue57823.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue5856.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run From 06d460f6aac44c35b9e206300fb2e4eb300ee9de Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 12 Jul 2026 20:34:01 +0800 Subject: [PATCH 3/7] cl: cover panic and defer pc-line anchors --- cl/defer_panic_pc_test.go | 145 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 cl/defer_panic_pc_test.go diff --git a/cl/defer_panic_pc_test.go b/cl/defer_panic_pc_test.go new file mode 100644 index 0000000000..0deb3d0aef --- /dev/null +++ b/cl/defer_panic_pc_test.go @@ -0,0 +1,145 @@ +//go:build !llgo +// +build !llgo + +package cl + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + "testing" + + "golang.org/x/tools/go/ssa" +) + +func TestDeferRunPos(t *testing.T) { + ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo + +func declared() {} + +func outer() { + func() {}() +} +`) + fallback := token.Pos(1) + + declFn := ssapkg.Func("declared") + decl, ok := declFn.Syntax().(*ast.FuncDecl) + if !ok { + t.Fatalf("declared syntax = %T, want *ast.FuncDecl", declFn.Syntax()) + } + ctx := &context{goFn: declFn} + if got := ctx.deferRunPos(fallback); got != decl.Body.Rbrace { + t.Fatalf("declaration defer position = %v, want closing brace %v", got, decl.Body.Rbrace) + } + + outer := ssapkg.Func("outer") + if len(outer.AnonFuncs) != 1 { + t.Fatalf("outer anonymous functions = %d, want 1", len(outer.AnonFuncs)) + } + litFn := outer.AnonFuncs[0] + lit, ok := litFn.Syntax().(*ast.FuncLit) + if !ok { + t.Fatalf("literal syntax = %T, want *ast.FuncLit", litFn.Syntax()) + } + ctx.goFn = litFn + if got := ctx.deferRunPos(fallback); got != lit.Body.Rbrace { + t.Fatalf("literal defer position = %v, want closing brace %v", got, lit.Body.Rbrace) + } + + declBody := decl.Body + decl.Body = nil + ctx.goFn = declFn + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("bodyless declaration position = %v, want fallback %v", got, fallback) + } + decl.Body = declBody + declRbrace := decl.Body.Rbrace + decl.Body.Rbrace = token.NoPos + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("declaration with invalid closing brace position = %v, want fallback %v", got, fallback) + } + decl.Body.Rbrace = declRbrace + + litBody := lit.Body + lit.Body = nil + ctx.goFn = litFn + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("bodyless literal position = %v, want fallback %v", got, fallback) + } + lit.Body = litBody + litRbrace := lit.Body.Rbrace + lit.Body.Rbrace = token.NoPos + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("literal with invalid closing brace position = %v, want fallback %v", got, fallback) + } + lit.Body.Rbrace = litRbrace + + ctx.goFn = &ssa.Function{} + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("function without syntax position = %v, want fallback %v", got, fallback) + } + ctx.goFn = nil + if got := ctx.deferRunPos(fallback); got != fallback { + t.Fatalf("nil function position = %v, want fallback %v", got, fallback) + } +} + +func TestCompileDeferAndPanicPCLineAnchors(t *testing.T) { + ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo +import "runtime" + +func withDefer() { + defer func() {}() + runtime.Caller(0) +} + +func withLiteralDefer() { + func() { + defer func() {}() + runtime.Caller(0) + }() +} + +func withPanic() { + runtime.Caller(0) + panic("boom") +} +`) + prog := newLLSSAProg(t) + prog.Target().GOOS = "linux" + prog.Target().GOARCH = "amd64" + prog.EnableFuncInfoMetadata(true) + prog.EnableFuncInfoSites(true) + pkg, err := NewPackage(prog, ssapkg, files) + if err != nil { + t.Fatal(err) + } + ir := pkg.Module().String() + + tests := []struct { + symbol string + line int + column int + }{ + {symbol: "example.com/foo.withDefer", line: 7, column: 1}, + {symbol: "example.com/foo.withLiteralDefer$1", line: 13, column: 2}, + {symbol: "example.com/foo.withPanic", line: 18, column: 7}, + } + for _, tt := range tests { + if !hasPCLineMetadataPosition(ir, tt.symbol, tt.line, tt.column) { + t.Fatalf("missing pc-line anchor for %s at caller_frame_compile.go:%d:%d:\n%s", tt.symbol, tt.line, tt.column, ir) + } + } +} + +func hasPCLineMetadataPosition(ir, symbol string, line, column int) bool { + want := fmt.Sprintf(`!%q, !"caller_frame_compile.go", i32 %d, i32 %d}`, symbol, line, column) + for _, row := range strings.Split(ir, "\n") { + if strings.Contains(row, "= !{i32 1, i64 ") && strings.Contains(row, want) { + return true + } + } + return false +} From 59dd7c5fe3884f5f89e52a205231a8206e6258e4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 27 Jul 2026 10:48:26 +0800 Subject: [PATCH 4/7] test: rely on end-to-end panic snapshot coverage --- cl/defer_panic_pc_test.go | 145 --------------------------------- test/llgoext/panic_pcs_test.go | 101 ----------------------- 2 files changed, 246 deletions(-) delete mode 100644 cl/defer_panic_pc_test.go delete mode 100644 test/llgoext/panic_pcs_test.go diff --git a/cl/defer_panic_pc_test.go b/cl/defer_panic_pc_test.go deleted file mode 100644 index 0deb3d0aef..0000000000 --- a/cl/defer_panic_pc_test.go +++ /dev/null @@ -1,145 +0,0 @@ -//go:build !llgo -// +build !llgo - -package cl - -import ( - "fmt" - "go/ast" - "go/token" - "strings" - "testing" - - "golang.org/x/tools/go/ssa" -) - -func TestDeferRunPos(t *testing.T) { - ssapkg, _ := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo - -func declared() {} - -func outer() { - func() {}() -} -`) - fallback := token.Pos(1) - - declFn := ssapkg.Func("declared") - decl, ok := declFn.Syntax().(*ast.FuncDecl) - if !ok { - t.Fatalf("declared syntax = %T, want *ast.FuncDecl", declFn.Syntax()) - } - ctx := &context{goFn: declFn} - if got := ctx.deferRunPos(fallback); got != decl.Body.Rbrace { - t.Fatalf("declaration defer position = %v, want closing brace %v", got, decl.Body.Rbrace) - } - - outer := ssapkg.Func("outer") - if len(outer.AnonFuncs) != 1 { - t.Fatalf("outer anonymous functions = %d, want 1", len(outer.AnonFuncs)) - } - litFn := outer.AnonFuncs[0] - lit, ok := litFn.Syntax().(*ast.FuncLit) - if !ok { - t.Fatalf("literal syntax = %T, want *ast.FuncLit", litFn.Syntax()) - } - ctx.goFn = litFn - if got := ctx.deferRunPos(fallback); got != lit.Body.Rbrace { - t.Fatalf("literal defer position = %v, want closing brace %v", got, lit.Body.Rbrace) - } - - declBody := decl.Body - decl.Body = nil - ctx.goFn = declFn - if got := ctx.deferRunPos(fallback); got != fallback { - t.Fatalf("bodyless declaration position = %v, want fallback %v", got, fallback) - } - decl.Body = declBody - declRbrace := decl.Body.Rbrace - decl.Body.Rbrace = token.NoPos - if got := ctx.deferRunPos(fallback); got != fallback { - t.Fatalf("declaration with invalid closing brace position = %v, want fallback %v", got, fallback) - } - decl.Body.Rbrace = declRbrace - - litBody := lit.Body - lit.Body = nil - ctx.goFn = litFn - if got := ctx.deferRunPos(fallback); got != fallback { - t.Fatalf("bodyless literal position = %v, want fallback %v", got, fallback) - } - lit.Body = litBody - litRbrace := lit.Body.Rbrace - lit.Body.Rbrace = token.NoPos - if got := ctx.deferRunPos(fallback); got != fallback { - t.Fatalf("literal with invalid closing brace position = %v, want fallback %v", got, fallback) - } - lit.Body.Rbrace = litRbrace - - ctx.goFn = &ssa.Function{} - if got := ctx.deferRunPos(fallback); got != fallback { - t.Fatalf("function without syntax position = %v, want fallback %v", got, fallback) - } - ctx.goFn = nil - if got := ctx.deferRunPos(fallback); got != fallback { - t.Fatalf("nil function position = %v, want fallback %v", got, fallback) - } -} - -func TestCompileDeferAndPanicPCLineAnchors(t *testing.T) { - ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo -import "runtime" - -func withDefer() { - defer func() {}() - runtime.Caller(0) -} - -func withLiteralDefer() { - func() { - defer func() {}() - runtime.Caller(0) - }() -} - -func withPanic() { - runtime.Caller(0) - panic("boom") -} -`) - prog := newLLSSAProg(t) - prog.Target().GOOS = "linux" - prog.Target().GOARCH = "amd64" - prog.EnableFuncInfoMetadata(true) - prog.EnableFuncInfoSites(true) - pkg, err := NewPackage(prog, ssapkg, files) - if err != nil { - t.Fatal(err) - } - ir := pkg.Module().String() - - tests := []struct { - symbol string - line int - column int - }{ - {symbol: "example.com/foo.withDefer", line: 7, column: 1}, - {symbol: "example.com/foo.withLiteralDefer$1", line: 13, column: 2}, - {symbol: "example.com/foo.withPanic", line: 18, column: 7}, - } - for _, tt := range tests { - if !hasPCLineMetadataPosition(ir, tt.symbol, tt.line, tt.column) { - t.Fatalf("missing pc-line anchor for %s at caller_frame_compile.go:%d:%d:\n%s", tt.symbol, tt.line, tt.column, ir) - } - } -} - -func hasPCLineMetadataPosition(ir, symbol string, line, column int) bool { - want := fmt.Sprintf(`!%q, !"caller_frame_compile.go", i32 %d, i32 %d}`, symbol, line, column) - for _, row := range strings.Split(ir, "\n") { - if strings.Contains(row, "= !{i32 1, i64 ") && strings.Contains(row, want) { - return true - } - } - return false -} diff --git a/test/llgoext/panic_pcs_test.go b/test/llgoext/panic_pcs_test.go deleted file mode 100644 index 38936f4431..0000000000 --- a/test/llgoext/panic_pcs_test.go +++ /dev/null @@ -1,101 +0,0 @@ -//go:build llgo - -/* - * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package llgoext - -import ( - "reflect" - "testing" - _ "unsafe" -) - -//go:linkname runtimeStorePanicPCsForTest github.com/goplus/llgo/runtime/internal/runtime.StorePanicPCs -func runtimeStorePanicPCsForTest([]uintptr) - -//go:linkname runtimeStoreFaultPCsForTest github.com/goplus/llgo/runtime/internal/runtime.StoreFaultPCs -func runtimeStoreFaultPCsForTest([]uintptr) - -//go:linkname runtimePanicPCsForTest github.com/goplus/llgo/runtime/internal/runtime.PanicPCs -func runtimePanicPCsForTest() []uintptr - -//go:linkname runtimePanicPCsAreFaultForTest github.com/goplus/llgo/runtime/internal/runtime.PanicPCsAreFault -func runtimePanicPCsAreFaultForTest() bool - -//go:linkname runtimeMarkPanicRecoverFPsForTest github.com/goplus/llgo/runtime/internal/runtime.MarkPanicRecoverFPs -func runtimeMarkPanicRecoverFPsForTest(uintptr, uintptr) - -//go:linkname runtimePanicRecoverFPsForTest github.com/goplus/llgo/runtime/internal/runtime.PanicRecoverFPs -func runtimePanicRecoverFPsForTest() (uintptr, uintptr) - -type panicPCState struct { - pcs []uintptr - fault bool - recover1 uintptr - recover2 uintptr -} - -func TestRuntimePanicPCStateIsolation(t *testing.T) { - runtimeStorePanicPCsForTest([]uintptr{11, 12}) - runtimeMarkPanicRecoverFPsForTest(13, 14) - - start := make(chan struct{}) - results := make(chan panicPCState, 2) - for i := uintptr(0); i < 2; i++ { - go func(base uintptr) { - <-start - runtimeStoreFaultPCsForTest([]uintptr{base, base + 1}) - runtimeMarkPanicRecoverFPsForTest(base+2, base+3) - recover1, recover2 := runtimePanicRecoverFPsForTest() - results <- panicPCState{ - pcs: append([]uintptr(nil), runtimePanicPCsForTest()...), - fault: runtimePanicPCsAreFaultForTest(), - recover1: recover1, - recover2: recover2, - } - }(21 + i*10) - } - close(start) - - seen := make(map[uintptr]panicPCState) - for i := 0; i < 2; i++ { - state := <-results - seen[state.pcs[0]] = state - } - for _, base := range []uintptr{21, 31} { - state, ok := seen[base] - if !ok { - t.Fatalf("missing goroutine state for base %d: %#v", base, seen) - } - if want := []uintptr{base, base + 1}; !reflect.DeepEqual(state.pcs, want) { - t.Fatalf("pcs for base %d = %v, want %v", base, state.pcs, want) - } - if !state.fault || state.recover1 != base+2 || state.recover2 != base+3 { - t.Fatalf("state for base %d = %#v", base, state) - } - } - - if got := runtimePanicPCsForTest(); !reflect.DeepEqual(got, []uintptr{11, 12}) { - t.Fatalf("main goroutine pcs = %v, want [11 12]", got) - } - if runtimePanicPCsAreFaultForTest() { - t.Fatal("main goroutine snapshot unexpectedly marked as fault") - } - if recover1, recover2 := runtimePanicRecoverFPsForTest(); recover1 != 13 || recover2 != 14 { - t.Fatalf("main goroutine recover marks = (%d, %d), want (13, 14)", recover1, recover2) - } -} From d55903033bddb7de66c31e92c7d7ca8b3d386f62 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 27 Jul 2026 10:48:27 +0800 Subject: [PATCH 5/7] runtime: define no-frame-pointer unwind hooks --- .../internal/lib/runtime/unwind_baremetal_llgo.go | 15 +++++++++++++++ runtime/internal/lib/runtime/unwind_wasm_llgo.go | 6 ++++++ 2 files changed, 21 insertions(+) create mode 100644 runtime/internal/lib/runtime/unwind_baremetal_llgo.go diff --git a/runtime/internal/lib/runtime/unwind_baremetal_llgo.go b/runtime/internal/lib/runtime/unwind_baremetal_llgo.go new file mode 100644 index 0000000000..04d2617178 --- /dev/null +++ b/runtime/internal/lib/runtime/unwind_baremetal_llgo.go @@ -0,0 +1,15 @@ +//go:build baremetal && !wasm + +package runtime + +func fpCallers(_ int, _ []uintptr) int { + return 0 +} + +func fpUnwindAvailable() bool { + return false +} + +func callersWithPanicSplice(_ int, _ []uintptr) int { + return 0 +} diff --git a/runtime/internal/lib/runtime/unwind_wasm_llgo.go b/runtime/internal/lib/runtime/unwind_wasm_llgo.go index fb59d5e063..5ed4125531 100644 --- a/runtime/internal/lib/runtime/unwind_wasm_llgo.go +++ b/runtime/internal/lib/runtime/unwind_wasm_llgo.go @@ -10,3 +10,9 @@ func fpCallers(skip int, pc []uintptr) int { func fpUnwindAvailable() bool { return false } + +// callersWithPanicSplice is unreachable while fpUnwindAvailable is false, +// but it must remain in the selected source set for extern.go to type-check. +func callersWithPanicSplice(_ int, _ []uintptr) int { + return 0 +} From d6344128b3ec8677752d725109afc2c76b907f09 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 27 Jul 2026 17:50:00 +0800 Subject: [PATCH 6/7] test: isolate tiny finalizers from conservative roots --- test/go/finalizer_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/go/finalizer_test.go b/test/go/finalizer_test.go index aceb2d2a2c..ec986129d9 100644 --- a/test/go/finalizer_test.go +++ b/test/go/finalizer_test.go @@ -25,7 +25,12 @@ import ( func TestRuntimeSetFinalizerTinyObjects(t *testing.T) { const n = 32 finalized := make(chan int32, n) - makeFinalizerTinyObjects(n, finalized) + created := make(chan struct{}) + go func() { + makeFinalizerTinyObjects(n, finalized) + close(created) + }() + <-created done := make([]bool, n) count := 0 From bf9389ff58f5d89a94478c518709175ec19827a4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 27 Jul 2026 17:50:11 +0800 Subject: [PATCH 7/7] test: refresh GOROOT expectations after merged fixes --- test/goroot/xfail.yaml | 138 ++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 93 deletions(-) diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index ef3b9bfd1f..3c66ac0ff1 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -1772,6 +1772,51 @@ flakes: directive: run case: goprint.go reason: darwin/arm64 goroot run can either pass or fail + - version: go1.24 + platform: linux/amd64 + directive: run + case: fixedbugs/issue45045.go + reason: replaced map keys are not reliably finalized on linux/amd64, so the run can pass or time out waiting for the old key + - version: go1.25 + platform: linux/amd64 + directive: run + case: fixedbugs/issue45045.go + reason: replaced map keys are not reliably finalized on linux/amd64, so the run can pass or time out waiting for the old key + - version: go1.26 + platform: linux/amd64 + directive: run + case: fixedbugs/issue45045.go + reason: replaced map keys are not reliably finalized on linux/amd64, so the run can pass or time out waiting for the old key + - version: go1.24 + platform: linux/amd64 + directive: run + case: fixedbugs/issue54343.go + reason: the bound-method receiver is not reliably finalized after its method value is cleared on linux/amd64, so the run can pass or report never GC'd + - version: go1.25 + platform: linux/amd64 + directive: run + case: fixedbugs/issue54343.go + reason: the bound-method receiver is not reliably finalized after its method value is cleared on linux/amd64, so the run can pass or report never GC'd + - version: go1.26 + platform: linux/amd64 + directive: run + case: fixedbugs/issue54343.go + reason: the bound-method receiver is not reliably finalized after its method value is cleared on linux/amd64, so the run can pass or report never GC'd + - version: go1.24 + platform: darwin/arm64 + directive: run + case: fixedbugs/issue25897a.go + reason: reflect-generated goroutines under a single-P continuous-GC loop can complete or exceed the 1m run timeout on darwin/arm64 + - version: go1.25 + platform: darwin/arm64 + directive: run + case: fixedbugs/issue25897a.go + reason: reflect-generated goroutines under a single-P continuous-GC loop can complete or exceed the 1m run timeout on darwin/arm64 + - version: go1.26 + platform: darwin/arm64 + directive: run + case: fixedbugs/issue25897a.go + reason: reflect-generated goroutines under a single-P continuous-GC loop can complete or exceed the 1m run timeout on darwin/arm64 xfails: - version: go1.26 directive: errorcheck @@ -2389,11 +2434,6 @@ xfails: directive: run case: finprofiled.go reason: LLGo finalizer profiling run hangs beyond the 1m timeout on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue25897a.go - reason: LLGo makes no progress starting reflect-generated goroutines while a single-P runtime continuously collects - version: go1.24 platform: darwin/arm64 directive: run @@ -2424,10 +2464,6 @@ xfails: directive: run case: fixedbugs/issue72063.go reason: generic Y-combinator build exits successfully without producing a binary on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue72844.go - reason: dereferencing a nil array pointer for len or range does not panic when required on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2660,11 +2696,6 @@ xfails: directive: run case: fixedbugs/issue65417.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue72844.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2720,11 +2751,6 @@ xfails: directive: run case: fixedbugs/issue65417.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue72844.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -2840,20 +2866,6 @@ xfails: directive: run case: fixedbugs/issue32477.go reason: latest main goroot run failure on darwin/arm64 - - version: go1.24 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue45045.go - reason: latest main goroot run failure on darwin/arm64 - - version: go1.25 - platform: darwin/arm64 - directive: run - case: fixedbugs/issue45045.go - reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue45045.go - reason: latest main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue4562.go @@ -2862,10 +2874,6 @@ xfails: directive: run case: fixedbugs/issue46725.go reason: latest main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue54343.go - reason: latest main goroot run failure on darwin/arm64 - version: go1.24 platform: darwin/arm64 directive: run @@ -2949,11 +2957,6 @@ xfails: directive: run case: fixedbugs/issue4066.go reason: go1.25 goroot run failure on darwin/arm64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue45045.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -2969,11 +2972,6 @@ xfails: directive: run case: fixedbugs/issue72860.go reason: go1.25 goroot run failure on linux/amd64 - - version: go1.25 - platform: linux/amd64 - directive: run - case: fixedbugs/issue54343.go - reason: go1.25 goroot run failure on linux/amd64 - version: go1.25 platform: linux/amd64 directive: run @@ -3075,11 +3073,6 @@ xfails: directive: run case: fixedbugs/issue46725.go reason: go1.24 goroot run failure on linux/amd64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue54343.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3150,11 +3143,6 @@ xfails: directive: run case: fixedbugs/issue4066.go reason: go1.24 goroot run failure on darwin/arm64 - - version: go1.24 - platform: linux/amd64 - directive: run - case: fixedbugs/issue45045.go - reason: go1.24 goroot run failure on linux/amd64 - version: go1.24 platform: linux/amd64 directive: run @@ -3185,11 +3173,6 @@ xfails: directive: run case: maymorestack.go reason: go1.26 goroot ci-mode run failure on darwin/arm64 - - version: go1.26 - platform: darwin/arm64 - directive: run - case: devirtualization_nil_panics.go - reason: go1.26 goroot run failure on darwin/arm64 - version: go1.26 platform: linux/amd64 directive: run @@ -3260,11 +3243,6 @@ xfails: directive: run case: fixedbugs/issue4066.go reason: go1.26 goroot run failure on darwin/arm64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue45045.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3280,11 +3258,6 @@ xfails: directive: run case: fixedbugs/issue5493.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: devirtualization_nil_panics.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3325,11 +3298,6 @@ xfails: directive: run case: fixedbugs/issue46725.go reason: go1.26 goroot run failure on linux/amd64 - - version: go1.26 - platform: linux/amd64 - directive: run - case: fixedbugs/issue54343.go - reason: go1.26 goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3394,10 +3362,6 @@ xfails: directive: run case: fixedbugs/issue43835.go reason: current main goroot run failure on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue47928.go - reason: current main goroot run failure on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue38496.go @@ -3478,10 +3442,6 @@ xfails: directive: run case: fixedbugs/issue43835.go reason: current main goroot run failure on linux/amd64 - - platform: linux/amd64 - directive: run - case: fixedbugs/issue47928.go - reason: current main goroot run failure on linux/amd64 - version: go1.26 platform: linux/amd64 directive: run @@ -3577,18 +3537,10 @@ xfails: directive: rundir case: fixedbugs/issue24693.go reason: llgo constructs cross-package itabs incorrectly for unexported interface methods - - version: go1.26 - directive: runindir - case: fixedbugs/issue11656.go - reason: llgo traceback metadata cannot resolve the recursive caller after a bad-PC fault - version: go1.26 directive: rundir case: fixedbugs/issue18911.go reason: llgo interface-conversion diagnostics omit the same-looking types from different packages detail - - version: go1.26 - directive: runindir - case: fixedbugs/issue30862.go - reason: llgo does not implement the fieldtrack experiment metadata used by this test - version: go1.26 directive: rundir case: interface/embed3.go