Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions bind/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions bind/gen_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions cmd_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmd_exe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`")

Expand Down Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions cmd_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmd_pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`")

Expand Down Expand Up @@ -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]
Expand Down
Loading