From f29e6de426ed94dfc49d204c4d456f4cb0d95571 Mon Sep 17 00:00:00 2001 From: Vadim Dyadkin Date: Thu, 23 Jul 2026 11:10:36 +0200 Subject: [PATCH] bind: make _gopy_clear_go_tls opt-in, off by default The unconditional _gopy_clear_go_tls() call (issue #370) performs a hardcoded TLS store (movq $0, %fs:-8 on linux/amd64). On glibc + CPython 3.12+ that offset overlaps CPython's current-thread-state TLS slot, so the store nulls it and the interpreter segfaults on the first CGo entry in the common single-extension case (issue #395). Add a -clear-go-tls build flag (default false) and gate the single call site on it. The C helper and its pybindgen registration are left in place so opt-in restores the previous behavior exactly. RTLD_GLOBAL-local loading, which is what actually isolates each runtime's goroutine-pointer TLS, is untouched and remains unconditional. Fixes #395 --- bind/gen.go | 24 ++++++++++++++++++------ bind/gen_func.go | 9 +++++++-- cmd_build.go | 2 ++ cmd_exe.go | 2 ++ cmd_gen.go | 2 ++ cmd_pkg.go | 2 ++ 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/bind/gen.go b/bind/gen.go index 97c1ade6..f6d8604e 100644 --- a/bind/gen.go +++ b/bind/gen.go @@ -329,12 +329,13 @@ cwd = os.getcwd() currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) os.chdir(currentdir) # When multiple gopy extensions coexist in one Python process each carries its own -# independent Go runtime. The Go extension is loaded without RTLD_GLOBAL below, and -# _gopy_clear_go_tls() is called before each CGo entry to force needm() to run, which -# establishes the correct per-extension M/P/mcache context (issue #370). -# Also load the extension without RTLD_GLOBAL so that Go runtime symbols stay -# local to each .so — belt-and-suspenders on platforms where RTLD_GLOBAL is the -# Python default (e.g. some Linux builds). +# independent Go runtime. Loading each extension without RTLD_GLOBAL below keeps its +# Go runtime symbols (including the per-runtime goroutine-pointer TLS slot) local to +# its own .so, which is what prevents the runtimes from colliding (issue #370). +# A _gopy_clear_go_tls() call before each CGo entry is available as an extra safety +# net but is opt-in (gopy build -clear-go-tls), off by default: on glibc + CPython +# 3.12+ its hardcoded TLS store clobbers the interpreter thread state and crashes on +# the first call in the common single-extension case (issue #395). if hasattr(sys, 'getdlopenflags'): try: import ctypes as _gopy_ctypes @@ -518,6 +519,17 @@ var NoWarn = false // NoMake turns off generation of Makefiles var NoMake = false +// ClearGoTLS controls whether generated wrappers emit a _gopy_clear_go_tls() +// call before every CGo entry point (issue #370). It is opt-in and off by +// default. The clear performs a hardcoded TLS store (movq $0, %gs:0x30 on +// darwin/amd64, movq $0, %fs:-8 on linux/amd64); with a single gopy extension +// in the process it is unnecessary, and on glibc + CPython 3.12+ that offset +// overlaps the TLS slot CPython uses for the current thread state, so the store +// nulls it and the interpreter segfaults on the first call (issue #395). +// Loading each extension without RTLD_GLOBAL, done unconditionally, already +// keeps each runtime's goroutine-pointer TLS local to its own .so. +var ClearGoTLS = false + // GenPyBind generates a .go file, build.py file to enable pybindgen to create python bindings, // and wrapper .py file(s) that are loaded as the interface to the package with shadow // python-side classes diff --git a/bind/gen_func.go b/bind/gen_func.go index fc2ee15d..b2643343 100644 --- a/bind/gen_func.go +++ b/bind/gen_func.go @@ -339,8 +339,13 @@ if __err != nil { // Clear the Go TLS goroutine slot before the CGo entry point so that // Go's needm() runs and establishes the correct per-extension context. // Without this, two extensions sharing the same process can corrupt - // each other's heap via TLS collision (issue #370). - g.pywrap.Printf("_%s._gopy_clear_go_tls()\n", pkgname) + // each other's heap via TLS collision (issue #370). Opt-in and off by + // default: the hardcoded TLS store crashes CPython 3.12+ in the common + // single-extension case (issue #395), and RTLD_GLOBAL-local loading + // already isolates each runtime's goroutine-pointer TLS. + if ClearGoTLS { + g.pywrap.Printf("_%s._gopy_clear_go_tls()\n", pkgname) + } // pywrap output mnm := fsym.ID() diff --git a/cmd_build.go b/cmd_build.go index 3def118a..7e38997f 100644 --- a/cmd_build.go +++ b/cmd_build.go @@ -45,6 +45,7 @@ ex: cmd.Flag.Bool("symbols", true, "include symbols in output") cmd.Flag.Bool("no-warn", false, "suppress warning messages, which may be expected") cmd.Flag.Bool("no-make", false, "do not generate a Makefile, e.g., when called from Makefile") + cmd.Flag.Bool("clear-go-tls", false, "emit a _gopy_clear_go_tls() call before every CGo entry (issue #370); off by default, needed only when several gopy extensions share one process and known to crash CPython 3.12+ (issue #395)") cmd.Flag.Bool("dynamic-link", false, "whether to link output shared library dynamically to Python") cmd.Flag.String("build-tags", "", "build tags to be passed to `go build`") return cmd @@ -72,6 +73,7 @@ func gopyRunCmdBuild(cmdr *commander.Command, args []string) error { bind.NoWarn = cfg.NoWarn bind.NoMake = cfg.NoMake + bind.ClearGoTLS = cmdr.Flag.Lookup("clear-go-tls").Value.Get().(bool) for _, path := range args { bpkg, err := loadPackage(path, true, cfg.BuildTags) // build first diff --git a/cmd_exe.go b/cmd_exe.go index 60ee3ced..acbd51e4 100644 --- a/cmd_exe.go +++ b/cmd_exe.go @@ -58,6 +58,7 @@ ex: cmd.Flag.String("url", "https://github.com/go-python/gopy", "home page for project") cmd.Flag.Bool("no-warn", false, "suppress warning messages, which may be expected") cmd.Flag.Bool("no-make", false, "do not generate a Makefile, e.g., when called from Makefile") + cmd.Flag.Bool("clear-go-tls", false, "emit a _gopy_clear_go_tls() call before every CGo entry (issue #370); off by default, needed only when several gopy extensions share one process and known to crash CPython 3.12+ (issue #395)") cmd.Flag.Bool("dynamic-link", false, "whether to link output shared library dynamically to Python") cmd.Flag.String("build-tags", "", "build tags to be passed to `go build`") @@ -97,6 +98,7 @@ func gopyRunCmdExe(cmdr *commander.Command, args []string) error { bind.NoWarn = cfg.NoWarn bind.NoMake = cfg.NoMake + bind.ClearGoTLS = cmdr.Flag.Lookup("clear-go-tls").Value.Get().(bool) if cfg.Name == "" { path := args[0] diff --git a/cmd_gen.go b/cmd_gen.go index becd0f1b..8b767354 100644 --- a/cmd_gen.go +++ b/cmd_gen.go @@ -37,6 +37,7 @@ ex: cmd.Flag.Bool("rename", false, "rename Go symbols to python PEP snake_case") cmd.Flag.Bool("no-warn", false, "suppress warning messages, which may be expected") cmd.Flag.Bool("no-make", false, "do not generate a Makefile, e.g., when called from Makefile") + cmd.Flag.Bool("clear-go-tls", false, "emit a _gopy_clear_go_tls() call before every CGo entry (issue #370); off by default, needed only when several gopy extensions share one process and known to crash CPython 3.12+ (issue #395)") cmd.Flag.Bool("dynamic-link", false, "whether to link output shared library dynamically to Python") cmd.Flag.String("build-tags", "", "build tags to be passed to `go build`") return cmd @@ -69,6 +70,7 @@ func gopyRunCmdGen(cmdr *commander.Command, args []string) error { bind.NoWarn = cfg.NoWarn bind.NoMake = cfg.NoMake + bind.ClearGoTLS = cmdr.Flag.Lookup("clear-go-tls").Value.Get().(bool) for _, path := range args { bpkg, err := loadPackage(path, true, cfg.BuildTags) // build first diff --git a/cmd_pkg.go b/cmd_pkg.go index 0f58480c..9891907c 100644 --- a/cmd_pkg.go +++ b/cmd_pkg.go @@ -55,6 +55,7 @@ ex: cmd.Flag.String("url", "https://github.com/go-python/gopy", "home page for project") cmd.Flag.Bool("no-warn", false, "suppress warning messages, which may be expected") cmd.Flag.Bool("no-make", false, "do not generate a Makefile, e.g., when called from Makefile") + cmd.Flag.Bool("clear-go-tls", false, "emit a _gopy_clear_go_tls() call before every CGo entry (issue #370); off by default, needed only when several gopy extensions share one process and known to crash CPython 3.12+ (issue #395)") cmd.Flag.Bool("dynamic-link", false, "whether to link output shared library dynamically to Python") cmd.Flag.String("build-tags", "", "build tags to be passed to `go build`") @@ -93,6 +94,7 @@ func gopyRunCmdPkg(cmdr *commander.Command, args []string) error { bind.NoWarn = cfg.NoWarn bind.NoMake = cfg.NoMake + bind.ClearGoTLS = cmdr.Flag.Lookup("clear-go-tls").Value.Get().(bool) if cfg.Name == "" { path := args[0]