fix(linux): use cgo bridge for dynamic system symbols - #73
Conversation
Linux CGO builds currently route dlopen, dlsym, dlerror and __errno_location through assembly stubs that branch directly to symbols declared with //go:cgo_import_dynamic. In larger consumer binaries that relocation can reach cmd/link as R_CALL -> SDYNIMPORT, which the linker rejects before the external linker can run. Use real C wrappers for Linux CGO builds instead. The new assembly-free internal/cbridge package calls libc/libdl through import C, while internal/dl and internal/syscall retain their existing Go-facing APIs. The current assembly stubs and cgo_import_dynamic implementation remain unchanged for Linux !cgo and for Darwin, FreeBSD, and Android paths. The cbridge indirection is required because Go rejects packages that contain both import C and Go assembly. Tests cover libc loading, stable symbol resolution, invalid-library errors, and the errno accessor. Validated against a real downstream consumer that previously reproduced go-webgpu#72: clr-aistudioinstaller/cmd/cli now links on Linux amd64 and arm64 when replaced with this checkout. Fixes go-webgpu#72
|
Tested in my project using the line below and worked.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Thanks for the clear write-up and for validating this against a real consumer — the root cause analysis matches what we see in On this approachRouting Linux Recommendation: prefer a no-
|
….Dlerror Addresses the three minor nits flagged in the go-webgpu#73 review. - RTLD_* constants were duplicated between dl_linux.go (!cgo) and dl_linux_cgo.go (cgo). Extract them into dl_linux_consts.go (//go:build linux && !android) so both paths share the same values. Darwin and FreeBSD were not affected; their constants live in single per-platform files. - cbridge.Dlerror was unused after the cgo bridge landed in go-webgpu#73 (errors are embedded in goffi_dl_result and reformatted inside dl_linux_cgo.go). Remove it to keep the public surface intentional. - The stale build-tag comments in dl_stubs_unix.s / dl_stubs_arm64.s mentioned dl_linux_nocgo.go / dl_darwin_nocgo.go. Those references were already updated in 6772a59 (this branch's leading commit); no further action. The dl_unix.go block comment is updated to point at the new file.
|
Thanks for the detailed review and for the alternative prototype — the Follow-ups just pushed on this branch (commit
What I'm not doing in this PR:
Branch |
kolkov
left a comment
There was a problem hiding this comment.
Thanks for tracking this down and for the real downstream reproducer — the R_CALL → SDYNIMPORT linker failure is a genuine problem that needs fixing.
That said, we can't merge the import "C" / internal/cbridge approach. goffi's entire reason for existing is zero-CGO FFI — we added CGO_ENABLED=1 support as a concession for users who can't avoid it (race detector, coexistence with other CGO libraries), but the contract is that goffi itself doesn't bring C code into the build. Adding import "C" breaks that contract: any consumer that pulls goffi with CGO_ENABLED=1 would now need a C compiler to build goffi's own internals, not just their own code.
@lkmavi's prototype in the comments — resolving loader symbols via //go:linkname to external linker symbols, then feeding those addresses into the existing runtime.cgocall wrappers — is the direction we want to go. When CGO_ENABLED=1, the external linker already sees dlopen/dlsym/__errno_location from libc; we just need their addresses without going through //go:cgo_import_dynamic assembly stubs that trigger the SDYNIMPORT relocation. No C wrappers, no separate cbridge package.
Roughly, the pattern we're looking at for the linux && cgo path:
//go:build linux && !android && cgo
package dl
//go:linkname libc_dlopen dlopen
var libc_dlopen byte
//go:linkname libc_dlsym dlsym
var libc_dlsym byte
//go:linkname libc_dlerror dlerror
var libc_dlerror byte
func init() {
// The external linker resolves these symbols from libc.
// We take their addresses and feed them into the existing
// runtime.cgocall wrappers — same shape as the !cgo stub ABI0 pointers.
dlopen_stub_addr = uintptr(unsafe.Pointer(&libc_dlopen))
dlsym_stub_addr = uintptr(unsafe.Pointer(&libc_dlsym))
dlerror_stub_addr = uintptr(unsafe.Pointer(&libc_dlerror))
}For errno, since //go:linkname to __errno_location still hits SDYNIMPORT, resolve it at runtime:
errnoFn = dlsym(RTLD_DEFAULT, "__errno_location") // RTLD_DEFAULT == 0 on LinuxNo import "C", no cbridge package, no C compiler for goffi internals. The existing assembly wrappers and runtime.cgocall path stay as-is.
We're investigating this path now. @lkmavi already validated it in a Docker linux/arm64 smoke test with purego in the same binary. If you're interested in collaborating on the implementation, we can coordinate on the test matrix (amd64/arm64 × glibc/musl). Otherwise we'll pick it up on our side.
The separate CGO_ENABLED=0 + purego fakecgo collision you mentioned is a known issue (#22, closed with workaround: -tags nofakecgo) — that's a different problem from this SDYNIMPORT relocation.
Appreciate the thorough analysis and the follow-up cleanup (RTLD dedup, dead code removal).
|
@besmpl — would appreciate your take on this. You've been deep in the Go linker internals with the Android fakecgo work and know the The core question: when The root cause is |
|
You're right on the zero-CGO contract. Adding I agree with the
Out of scope (confirming): #74 stays orthogonal. Per Two questions before I touch the codebase: a. PR #73 itself: do you want me to close it once the new PR is b. Approval to proceed: green light on the plan above? In I'll wait for your answers before pushing the new branch. If you'd |
Problem
Linux CGO consumers can fail during final executable linking when goffi's assembly stubs branch directly to symbols declared with
//go:cgo_import_dynamic:The failure happens inside
cmd/linkbefore the external linker can repair the relocation.Root cause
The Linux CGO path currently shares the zero-CGO implementation:
SDYNIMPORTsymbols;dlopen/dlsym/dlerrorare imported fromlibdl.so.2;__errno_locationis imported fromlibc.so.6.Changing the branch instruction or forcing external link mode does not help: Go still preprocesses the unsupported assembly-to-SDYNIMPORT relocation first.
Fix
internal/cbridgepackage for Linux CGO builds.dlopen,dlsym,dlerror, and__errno_locationthrough true C wrappers there.internal/dlandinternal/syscallGo APIs via small Linux-CGO adapters.cgo_import_dynamicimplementation to Linux!cgo.!cgobehavior unchanged.A separate
cbridgepackage is required because Go does not permitimport "C"in packages that also contain Go assembly.Validation
gofmt -l .: cleangit diff --check: cleaninternal/cbridge,internal/dl, andinternal/syscall: passgo test ./...: passCGO_ENABLED=0amd64/arm64internal/dl+internal/syscall: pass, proving the zero-CGO path remainsinditex/clr-aistudioinstaller/cmd/cli): links successfully on Linux arm64 with this checkout; the identical patch proof also links on Linux amd64The existing Darwin ARM64 full-suite
TestExecuteCaptureRegistersSimplefailure also reproduces on an untouchedupstream/mainarchive and is unrelated to this change.developcurrently points to v0.2.0-era sources and lacks the affected v0.6.x files, so this PR targetsmain, where the issue and v0.6.3 implementation live.Fixes #72