Skip to content

Commit 0299311

Browse files
committed
ci: add symbol diagnostic step for macOS Intel
On macos-15-intel, two separately-built gopy extensions loaded in the same Python process can crash with "fatal error: bad sweepgen in refill" on certain Go versions. The root cause is not yet confirmed: candidate mechanisms include PLT-based CGo symbol interposition (crosscall2, _cgo_topofstack, x_cgo_inittls, etc.) and/or dyld global-namespace deduplication of the ~150 runtime symbols exported by both .so files. Add a diagnostic step that runs on every macos-15-intel job (pass or fail) and reports three things: 1. how many dynamic symbols are shared between the two extensions 2. which of the critical CGo bridge symbols appear in the indirect symbol table (otool -Iv) — the macOS equivalent of JUMP_SLOT/PLT 3. which library wins in the global namespace at runtime (ctypes) Comparing the output across Go 1.21/1.22 (fail), 1.23/1.24 (pass), and 1.25 (fail) should confirm whether the crash correlates with PLT stub generation changes between Go versions.
1 parent 8740cc3 commit 0299311

1 file changed

Lines changed: 69 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,75 @@ jobs:
9494
run: |
9595
make test
9696
97+
- name: Symbol diagnostic (macOS Intel)
98+
if: always() && matrix.platform == 'macos-15-intel'
99+
continue-on-error: true
100+
env:
101+
GOFLAGS: "-mod=mod"
102+
run: |
103+
echo "=== building test packages for symbol analysis ==="
104+
go build -o /tmp/gopy_diag .
105+
DIAGDIR=$(mktemp -d)
106+
CWD=$(pwd)
107+
mkdir -p "$DIAGDIR/gilstring" "$DIAGDIR/simple"
108+
printf 'module dummy\nrequire github.com/go-python/gopy v0.0.0\nreplace github.com/go-python/gopy => %s\n' \
109+
"$CWD" > "$DIAGDIR/gilstring/go.mod"
110+
printf 'module dummy\nrequire github.com/go-python/gopy v0.0.0\nreplace github.com/go-python/gopy => %s\n' \
111+
"$CWD" > "$DIAGDIR/simple/go.mod"
112+
/tmp/gopy_diag build -vm=python3 -output="$DIAGDIR/gilstring" ./_examples/gilstring 2>&1 | tail -3
113+
/tmp/gopy_diag build -vm=python3 -output="$DIAGDIR/simple" ./_examples/simple 2>&1 | tail -3
114+
115+
GIL_SO=$(find "$DIAGDIR/gilstring" -name "_gilstring*.so" 2>/dev/null | head -1)
116+
SIM_SO=$(find "$DIAGDIR/simple" -name "_simple*.so" 2>/dev/null | head -1)
117+
echo "gilstring: $GIL_SO"
118+
echo "simple: $SIM_SO"
119+
[ -n "$GIL_SO" ] && [ -n "$SIM_SO" ] || { echo "SKIP: .so files not found"; exit 0; }
120+
121+
echo ""
122+
echo "=== shared dynamic symbols ==="
123+
nm "$GIL_SO" | awk '$2~/^[A-Z]$/{print $3}' | sort > /tmp/diag_g1.txt
124+
nm "$SIM_SO" | awk '$2~/^[A-Z]$/{print $3}' | sort > /tmp/diag_g2.txt
125+
echo "total shared: $(comm -12 /tmp/diag_g1.txt /tmp/diag_g2.txt | wc -l | tr -d ' ')"
126+
echo "critical CGo bridge symbols shared between both .so files:"
127+
comm -12 /tmp/diag_g1.txt /tmp/diag_g2.txt \
128+
| grep -E "crosscall|cgo_topofstack|x_cgo_init|x_cgo_inittls|cgo_yield|cgo_panic" \
129+
|| echo " (none)"
130+
131+
echo ""
132+
echo "=== indirect symbol table / PLT stubs in gilstring (otool -Iv) ==="
133+
TOTAL=$(otool -Iv "$GIL_SO" | grep -c '0x' || true)
134+
echo "total indirect entries: $TOTAL"
135+
otool -Iv "$GIL_SO" \
136+
| grep -E "crosscall|cgo_topofstack|x_cgo_init|x_cgo_inittls|cgo_yield|cgo_panic" \
137+
|| echo " (none of the critical CGo symbols appear as PLT stubs)"
138+
139+
echo ""
140+
echo "=== indirect symbol table / PLT stubs in simple (otool -Iv) ==="
141+
TOTAL=$(otool -Iv "$SIM_SO" | grep -c '0x' || true)
142+
echo "total indirect entries: $TOTAL"
143+
otool -Iv "$SIM_SO" \
144+
| grep -E "crosscall|cgo_topofstack|x_cgo_init|x_cgo_inittls|cgo_yield|cgo_panic" \
145+
|| echo " (none of the critical CGo symbols appear as PLT stubs)"
146+
147+
echo ""
148+
echo "=== runtime: which library wins in the global namespace ==="
149+
GIL_SO="$GIL_SO" SIM_SO="$SIM_SO" python3 << 'PYEOF'
150+
import ctypes, os
151+
GLOBAL = getattr(ctypes, 'RTLD_GLOBAL', 8)
152+
gil = ctypes.CDLL(os.environ['GIL_SO'], mode=GLOBAL)
153+
sim = ctypes.CDLL(os.environ['SIM_SO'], mode=GLOBAL)
154+
glb = ctypes.CDLL(None)
155+
for s in ['crosscall2', '_cgo_topofstack', 'x_cgo_init', 'x_cgo_inittls', '_cgo_yield', 'x_cgo_mmap']:
156+
try:
157+
a1 = ctypes.cast(getattr(gil, s), ctypes.c_void_p).value
158+
a2 = ctypes.cast(getattr(sim, s), ctypes.c_void_p).value
159+
ag = ctypes.cast(getattr(glb, s), ctypes.c_void_p).value
160+
who = 'gilstring' if ag == a1 else ('simple' if ag == a2 else 'other')
161+
print(' %-35s -> %-12s (global=%s)' % (s, who, hex(ag) if ag else 'N/A'))
162+
except Exception as e:
163+
print(' %-35s error: %s' % (s, e))
164+
PYEOF
165+
97166
- name: Upload-Coverage
98167
if: matrix.platform == 'ubuntu-latest'
99168
uses: codecov/codecov-action@v4

0 commit comments

Comments
 (0)