Skip to content

Commit 5072ba0

Browse files
committed
fix: restrict exported symbols in final .so to prevent Go runtime interposition
When two gopy extensions are loaded in the same Python process via RTLD_GLOBAL, Go runtime data globals (mcache0, allm, mheap_, etc.) from the first-loaded library win in the dynamic-linker global namespace. The second runtime's references to those globals are silently redirected, so both runtimes share the same heap metadata. This corrupts sweep- generation counters and causes: fatal error: bad sweepgen in refill on macOS x86_64 / Go ≥1.24 (the earlier pthread mutex fix serialised user code but could not stop background GC goroutines that also hit the shared globals). Fix: pass a symbol-visibility restriction to the final go build step so that only PyInit__<name> is exported into the global namespace: - macOS: -extldflags=-Wl,-exported_symbols_list,<file> - Linux: -extldflags=-Wl,--version-script,<file> All CGo bridge symbols (crosscall2, _cgo_topofstack, …) remain in the .so and are called directly at link time; they no longer pollute the global namespace and cannot be interposed by a second extension. Fixes #370 / #385.
1 parent 2f3f1ba commit 5072ba0

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

cmd_build.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,15 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error {
185185
if cfg.BuildTags != "" {
186186
args = append(args, "-tags", cfg.BuildTags)
187187
}
188+
// Collect all Go linker flags into one -ldflags argument so we can
189+
// combine -s/-w with the symbol-visibility extldflags below.
190+
var goLdFlags []string
188191
if !cfg.Symbols {
189192
// These flags will omit the various symbol tables, thereby
190193
// reducing the final size of the binary. From https://golang.org/cmd/link/
191194
// -s Omit the symbol table and debug information
192195
// -w Omit the DWARF symbol table
193-
args = append(args, "-ldflags=-s -w")
196+
goLdFlags = append(goLdFlags, "-s", "-w")
194197
}
195198
args = append(args, "-o", buildLib, ".")
196199
fmt.Printf("go %v\n", strings.Join(args, " "))
@@ -214,6 +217,42 @@ func runBuild(mode bind.BuildMode, cfg *BuildCfg) error {
214217
return err
215218
}
216219

220+
// Restrict exported symbols to only PyInit__<name> so that Go runtime
221+
// globals (e.g. runtime.mheap_, mcache0) are not placed in the global
222+
// dynamic-linker namespace. Two independently-loaded Go runtimes
223+
// sharing those globals via RTLD_GLOBAL interposition corrupt each
224+
// other's GC sweep-generation counters (issue #370 / #385).
225+
switch runtime.GOOS {
226+
case "darwin":
227+
// macOS exported_symbols_list: one symbol per line, with leading _.
228+
ef, ferr := os.CreateTemp("", "gopy-exports-*.txt")
229+
if ferr == nil {
230+
fmt.Fprintf(ef, "_%sPyInit__%s\n", "", cfg.Name)
231+
ef.Close()
232+
defer os.Remove(ef.Name())
233+
goLdFlags = append(goLdFlags,
234+
"-extldflags=-Wl,-exported_symbols_list,"+ef.Name())
235+
}
236+
case "linux":
237+
// GNU ld version script: export only PyInit__<name>.
238+
ef, ferr := os.CreateTemp("", "gopy-exports-*.map")
239+
if ferr == nil {
240+
fmt.Fprintf(ef, "{ global: PyInit__%s; local: *; };\n", cfg.Name)
241+
ef.Close()
242+
defer os.Remove(ef.Name())
243+
goLdFlags = append(goLdFlags,
244+
"-extldflags=-Wl,--version-script="+ef.Name())
245+
}
246+
}
247+
if len(goLdFlags) > 0 {
248+
// Replace or append the single combined -ldflags argument.
249+
// The args slice currently ends with "-o", modlib, "."
250+
// Insert before the final ".".
251+
combined := "-ldflags=" + strings.Join(goLdFlags, " ")
252+
n := len(args)
253+
args = append(args[:n-1], combined, args[n-1])
254+
}
255+
217256
if bind.WindowsOS {
218257
fmt.Printf("Doing windows sed hack to fix declspec for PyInit\n")
219258
fname := cfg.Name + ".c"

0 commit comments

Comments
 (0)