Skip to content

Commit ff0e140

Browse files
committed
Make tuple builder self-bracket the GIL
buildTuple no longer relies on callers to hold the GIL.
1 parent 9500152 commit ff0e140

2 files changed

Lines changed: 74 additions & 1 deletion

File tree

‎bind/symbols.go‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,19 @@ func (sym *symtab) buildTuple(tuple *types.Tuple, varnm string, methvar string)
636636
// bstr += fmt.Sprintf("}\n")
637637
// }
638638

639+
// PyTuple_New/PyTuple_SetItem below touch raw *C.PyObject values and
640+
// must run with the GIL held. This code is spliced verbatim into a
641+
// caller-generated function body, so it cannot assume the GIL is
642+
// already held there -- bracket it here instead of relying on every
643+
// caller to do so correctly (PyGILState_Ensure/Release nest safely, so
644+
// this is sound even when the caller already holds the GIL itself).
645+
// varnm is declared outside the block so it stays visible to
646+
// caller-emitted code that follows (e.g. PyObject_CallObject).
647+
//
639648
// TODO: more efficient to use strings.Builder here..
640-
bstr := fmt.Sprintf("%s := C.PyTuple_New(%d)\n", varnm, sz)
649+
bstr := fmt.Sprintf("var %s *C.PyObject\n{\n", varnm)
650+
bstr += "_tuple_gstate := C.PyGILState_Ensure()\n"
651+
bstr += fmt.Sprintf("%s = C.PyTuple_New(%d)\n", varnm, sz)
641652
for i := 0; i < sz; i++ {
642653
v := tuple.At(i)
643654
typ := v.Type()
@@ -679,6 +690,8 @@ func (sym *symtab) buildTuple(tuple *types.Tuple, varnm string, methvar string)
679690
return "", fmt.Errorf("buildTuple: type not handled: %s", typ.String())
680691
}
681692
}
693+
bstr += "C.PyGILState_Release(_tuple_gstate)\n"
694+
bstr += "}\n"
682695
return bstr, nil
683696
}
684697

‎bind/symbols_test.go‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2026 The go-python Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package bind
6+
7+
import (
8+
"go/token"
9+
"go/types"
10+
"strings"
11+
"testing"
12+
)
13+
14+
// TestBuildTupleHoldsGIL guards against a regression class already fixed
15+
// once for complex64/complex128 (see needsGILForArgMarshal in gen_func.go):
16+
// generated code that touches a raw *C.PyObject must never run outside a
17+
// PyGILState_Ensure/Release (or equivalent) bracket.
18+
//
19+
// buildTuple emits PyTuple_New/PyTuple_SetItem -- pure PyObject manipulation
20+
// -- as a bare code fragment with no way to know whether its caller already
21+
// holds the GIL. Its one current caller (addSignatureType's py2go closure)
22+
// happens to bracket it correctly today, but that's a convention external
23+
// to buildTuple, not something buildTuple enforces. This test requires
24+
// buildTuple's own output to be self-bracketing, so a future caller can't
25+
// reintroduce the complex64/128 bug by forgetting to hold the GIL.
26+
func TestBuildTupleHoldsGIL(t *testing.T) {
27+
tuple := types.NewTuple(
28+
types.NewVar(token.NoPos, nil, "i", types.Typ[types.Int]),
29+
types.NewVar(token.NoPos, nil, "s", types.Typ[types.String]),
30+
)
31+
32+
got, err := current.buildTuple(tuple, "_fcargs", "_fun_arg")
33+
if err != nil {
34+
t.Fatalf("buildTuple returned error: %v", err)
35+
}
36+
37+
ensureCount := strings.Count(got, "C.PyGILState_Ensure()")
38+
releaseCount := strings.Count(got, "C.PyGILState_Release(")
39+
if ensureCount == 0 || releaseCount == 0 {
40+
t.Fatalf("buildTuple output does not self-bracket its PyTuple_* calls "+
41+
"with PyGILState_Ensure/Release; got:\n%s", got)
42+
}
43+
if ensureCount != releaseCount {
44+
t.Fatalf("mismatched PyGILState_Ensure/Release counts (%d vs %d) in "+
45+
"buildTuple output:\n%s", ensureCount, releaseCount, got)
46+
}
47+
48+
ensureIdx := strings.Index(got, "C.PyGILState_Ensure()")
49+
firstTupleIdx := strings.Index(got, "C.PyTuple_")
50+
lastTupleIdx := strings.LastIndex(got, "C.PyTuple_")
51+
releaseIdx := strings.LastIndex(got, "C.PyGILState_Release(")
52+
53+
if ensureIdx == -1 || firstTupleIdx == -1 || releaseIdx == -1 {
54+
t.Fatalf("could not locate expected markers in buildTuple output:\n%s", got)
55+
}
56+
if !(ensureIdx < firstTupleIdx && lastTupleIdx < releaseIdx) {
57+
t.Fatalf("PyTuple_* calls are not nested inside the "+
58+
"PyGILState_Ensure/Release bracket in buildTuple output:\n%s", got)
59+
}
60+
}

0 commit comments

Comments
 (0)