Skip to content

Commit 58e1f43

Browse files
committed
fix(macos): drop overly-broad *NamedStruct heap-promotion; cross-platform stacktrace filter helper
- codegen/funcs.go: revert auto-promote of every *NamedStruct return as heap-owned. The check (added in e174b34) was too broad: user code that allocates via raw mem::calloc and returns *Tin-struct (e.g. an explicit list_node[T]::cons constructor) was treated as RC-owned, then ARC's scope-exit release_ptr ran on memory the user had already manually freed via mem::free -- double-free, intermittent hangs in test sequences (manifested on macOS recursive_generic_structs.tin). The await-call-site path (stmts.go from the same commit) still covers the channel-of-pointer leak that motivated the original change. Verified arc_stress + fiber + channel-pointer-stress valgrind output unchanged. - stdlib/source/source.tin: add is_program_entry to recognise main / start / _start / __libc_start_main as the OS-tail frames. On Linux these surface as libc.so.6:__libc_start_main and is_in_lib catches them; on macOS dladdr returns the program binary itself (no .dylib suffix) so the same frames slipped through. - examples/stacktrace_filter_pipe.tin: thread is_program_entry through in_user_code so the strict-subset assertion holds on macOS too.
1 parent 955f65d commit 58e1f43

3 files changed

Lines changed: 24 additions & 27 deletions

File tree

codegen/funcs.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,25 +2165,6 @@ func (cg *CodeGen) genFuncDeclAs(n *ast.FuncDecl, scopeName string) error {
21652165

21662166
heapPromoting := len(cg.curFnEscapingVars) > 0 || hasDirectHeapReturn(n.Body, cg.heapPromotingFns)
21672167

2168-
// Functions that return *NamedStruct transfer RC ownership to the
2169-
// caller -- the most common cases are constructor-like methods
2170-
// (`Cell.alloc`), container reads (`Channel.recv`, `Atomic.load`,
2171-
// `List.pop`), and parser/builder helpers. Mark them as heap-
2172-
// promoting so the call-site let-binding gets isHeapOwned=true and
2173-
// release_ptr fires at scope exit. Without this, `let c = ch.recv()`
2174-
// where ch is `Channel[*Cell[i64]]` would leak every dequeued cell.
2175-
if !heapPromoting && n.RetType != nil {
2176-
if rt, terr := cg.tinTypeToLLVM(n.RetType); terr == nil {
2177-
if pt, isPtr := rt.(*irtypes.PointerType); isPtr {
2178-
if innerSt, isStruct := pt.ElemType.(*irtypes.StructType); isStruct && innerSt.Name() != "" {
2179-
if _, isTinStruct := cg.structTypes[innerSt.Name()]; isTinStruct {
2180-
heapPromoting = true
2181-
}
2182-
}
2183-
}
2184-
}
2185-
}
2186-
21872168
if heapPromoting {
21882169
cg.heapPromotingFns[scopeName] = true
21892170
// Also store under the actual IR function name (which may include a

examples/stacktrace_filter_pipe.tin

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@ fn filter[t](pred fn(i t) bool) fn([t]) [t] =
2525

2626
fn{#no_inline} probe() [atom] = return stacktrace()
2727

28-
// Keep frames that have ANY identifying info (symbol or file) and are
29-
// NOT inside a shared library. Works on both Linux (libdwfl resolves
30-
// file:line:col) and macOS (no elfutils, frames are bare symbol+offset
31-
// with empty `file`) - the libc / libsystem tail is dropped via
32-
// `is_in_lib` on both platforms.
28+
// Keep frames that have ANY identifying info (symbol or file), are
29+
// NOT inside a shared library, and are NOT the libc/dyld program-entry
30+
// tail. The is_program_entry check is what makes the recipe portable:
31+
// on Linux dladdr returns libc.so.6 for __libc_start_main so is_in_lib
32+
// already catches it, but on macOS dladdr returns the program binary
33+
// (no .dylib suffix) for `start` and `main`, so they pass is_in_lib.
3334
fn in_user_code(f atom) bool =
3435
let p = source::parse_sourcepos(f)
35-
if source::is_unknown(p): return false
36-
if source::is_in_lib(p): return false
36+
if source::is_unknown(p): return false
37+
if source::is_in_lib(p): return false
38+
if source::is_program_entry(p): return false
3739
return source::is_resolved(p)
3840

3941
fn print_frames(label string, fs [atom]) =

stdlib/source/source.tin

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,24 @@ fn is_resolved(p SrcPos) bool =
241241
// (lib basename present). Useful for filtering libc / vendor frames.
242242
fn is_in_lib(p SrcPos) bool = return p.lib != ""
243243

244+
// is_program_entry reports whether the symbol is one of the standard
245+
// program-entry / loader frames that bracket every user trace. On Linux
246+
// these usually surface as `libc.so.6:__libc_start_main+0xN` (so
247+
// `is_in_lib` already drops them) but on macOS dladdr returns the
248+
// program binary itself (no .dylib suffix), so they pass `is_in_lib`.
249+
// Use this alongside `is_in_lib` when filtering out the system tail
250+
// in an OS-agnostic way.
251+
fn is_program_entry(p SrcPos) bool =
252+
if p.symbol == "main": return true
253+
if p.symbol == "_start": return true
254+
if p.symbol == "start": return true
255+
if strings::has_prefix(p.symbol, "__libc_start"): return true
256+
return false
257+
244258
// is_unknown reports whether the atom was the "??+0x<addr>" sentinel.
245259
fn is_unknown(p SrcPos) bool =
246260
return p.symbol == "" && p.file == "" && p.address != 0 as i64
247261

248262
export {
249-
SrcPos, parse_sourcepos, is_resolved, is_in_lib, is_unknown
263+
SrcPos, parse_sourcepos, is_resolved, is_in_lib, is_program_entry, is_unknown
250264
} as source

0 commit comments

Comments
 (0)