Skip to content

Fix memory leaks & crashes when loading multiple Go extensions in one Python process#393

Open
b-long wants to merge 5 commits into
masterfrom
fix/issue-370-and-issue-385
Open

Fix memory leaks & crashes when loading multiple Go extensions in one Python process#393
b-long wants to merge 5 commits into
masterfrom
fix/issue-370-and-issue-385

Conversation

@b-long
Copy link
Copy Markdown
Member

@b-long b-long commented May 13, 2026

Depends-on: #392
Fixes: #385
Fixes: #370


I took Scusemua's two commits in #361 as a base and built a broader set of fixes on top, covering memory leaks, GC coordination, and shared library conflicts.

This PR wouldn't exist without the work Scusemua put in first. His two commits from July 2024 fixed a subtle but nasty bug: Go code was calling a Python function without holding the GIL.

That's a precise fix that prevents several hard-to-reproduce crashes, and everything else here builds on top of it.


I added four fixes in the same area:

  1. C string memory leaks. Generated code allocated C strings when reading string fields from Go structs, slices, and maps, but never freed them. Switched those paths to a helper that frees after Python copies the value.

  2. Unsafe GC call. The generated wrapper was calling Go's GC from a thread that crosses the C/Go boundary, which could panic on multi-core machines. Rerouted it through a background goroutine via a channel.

  3. GC out of sync. Without coordination, Go heap objects piled up between Python GC cycles. Fixed by registering a gc.callbacks handler that triggers a Go collection after each Python one.

  4. Symbol conflicts between co-loaded extensions. Loading two gopy-built shared libraries with RTLD_GLOBAL caused one library's Go internals to clobber the other's, corrupting heap state. Fixed by loading without that flag so each library's runtime stays isolated.

  5. I added a regression test for two extensions in a single Python process, stealing from the discussion in issue 370.

I also expanded the CI matrix to cover more Go versions and macOS configs.

Scusemua and others added 5 commits May 7, 2026 20:05
…ne Python process

Several related bugs caused TestCStrings to fail and could crash programs
that import more than one gopy-built package at the same time:

1. C string leak in generated getters. When reading a string field from a
   Go struct, slice, or map, the generated code allocated a C string with
   C.CString() but never freed it. Over thousands of calls this accumulated
   enough memory to exceed the leak threshold in TestCStrings. Fixed by
   switching those accessors to use add_checked_string_function, which adds
   the missing free() after the Python string is built.

2. Unsafe Go GC call from a CGo thread. The generated wrapper called
   runtime.GC() directly while Python's garbage collector was running, which
   could panic with "bad sweepgen in refill" on multi-core machines. Fixed
   by routing the GC call through a background goroutine via a channel, so
   it always runs on a proper Go thread.

3. Go GC and Python GC out of sync. Without any coordination, Go heap
   objects could pile up between Python GC cycles, causing RSS to grow
   across test passes. The generated wrapper now registers a Python
   gc.callbacks handler that triggers a Go GC cycle after each Python GC
   cycle, keeping the two runtimes in sync automatically.

4. Symbol interposition between co-loaded extensions. On some platforms,
   loading two gopy .so files with RTLD_GLOBAL caused Go runtime symbols
   from one extension to override those in the other, corrupting heap state.
   Fixed by loading extensions without RTLD_GLOBAL and clearing the Go TLS
   slot before each CGo entry so each extension uses its own runtime context.

Also adds macos-15 (ARM) and macos-15-intel to the CI matrix, and includes
a new gilstring regression test that exercises two extensions in one process.
Copy link
Copy Markdown
Member

@rcoreilly rcoreilly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just from a quick read-through of the code, everything you have in here corresponds to what you described in the PR comment. Amazing work by multiple people to figure out these really arcane and random issues at the interface between these two languages -- I certainly never would have figured any of that out! And this approval by no means indicates that I understand the details of the loader and those needm assembly registers (wow!) but the tests don't lie presumably :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TestBindSimple and TestBindCgoPackage should not be skipped "broken" since go 1.21

3 participants