You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
unhandled relocation ... SDYNIMPORT when goffi and ebitengine/purego are linked into the same binary #72
Both packages independently arrange for //go:cgo_import_dynamic symbols to
be resolved by pulling in runtime/cgo (goffi explicitly, in internal/dl/cgo.go;
purego via its own internal/fakecgo/cgo.go). Something in having both paths
present in one link breaks resolution of goffi's dynamic imports specifically.
Repro attached as a tarball.
Downstream confirmation from f4: with CGO_ENABLED=0, goffi v0.6.3 is pulled into the normal Linux amd64/arm64 builds through internal/fakecgo, so -extldflags '-static' does not produce a fully static artifact. The regular cross-builds succeed, while a -tags noffi build removes the goffi/gogpu stack and can be static, but it also drops the gogpu, Wayland, and Ebiten backends.
This is tracked downstream in unxed/f4#693. PR #73 appears to address the Linux CGO relocation path; f4's affected release path is the !cgo fakecgo path, so the remaining dynamic-loader dependency needs an upstream design/fix rather than a silent f4-only backend removal.
Update 2026-10: the //go:linkname approach described below does not work — the variable defines its own dlopen symbol, so the call crashes (or the link fails when other C code in the program also calls dlopen). See the status update: #72 (comment)
Investigated this thoroughly. There are actually two separate problems here depending on CGO mode:
CGO_ENABLED=1 (the SDYNIMPORT error): The Go internal linker has a guard at cmd/link/internal/ld/data.go:255 that rejects R_CALL → SDYNIMPORT relocations on Linux. Darwin, AIX, Solaris, and OpenBSD are exempt — Linux isn't, even though the PLT machinery in amd64/asm.go:280 already handles it correctly. The guard is redundant. OpenBSD was added to the exception list via #42671 for the same reason (pthreads migration needed cgo_import_dynamic). We're preparing an upstream Go proposal to add Linux.
The immediate fix for goffi is to exclude the assembly JMP stubs (dl_stubs_unix.s) from cgo builds via !cgo build tag, and use //go:linkname variable indirection instead — the external linker resolves dlopen/dlsym directly from libc, so no SDYNIMPORT symbol exists in Go's link phase. This is exactly how purego handles it: dlfcn_stubs.s has !cgo tag, and internal/cgo/dlfcn_cgo_unix.go uses import "C" for the cgo path. @lkmavi validated this approach in a Docker linux/arm64 environment.
CGO_ENABLED=0 + purego in same binary: This is the existing fakecgo duplicate symbol issue (#22). Both goffi and purego define _cgo_init, _cgo_thread_start, etc. — Go has no dupok for data symbols. Workaround: -tags nofakecgo disables goffi's copy.
Working on the //go:linkname fix now. Will update when the PR is ready.
Status update, and a correction to my comment from August.
Root cause. This is not specific to purego. With CGO_ENABLED=1, goffi on Linux and FreeBSD fails to link whenever the final link is external: any package with import "C" in the same program, -ldflags=-linkmode=external, -buildmode=c-shared or c-archive. goffi reaches dlopen, dlsym, dlerror and the errno accessor through assembly stubs that jump to //go:cgo_import_dynamic symbols, and cmd/link rejects relocations to such symbols when it links externally (the SDYNIMPORT check in cmd/link/internal/ld/data.go; Darwin, OpenBSD, AIX and Solaris are exempt). goffi on its own links with the internal linker, which is why our CI never caught this. It has been this way since CGO_ENABLED=1 support landed in v0.5.1.
The //go:linkname plan from my August comment does not work. We tested it: //go:linkname libc_dlopen dlopen on var libc_dlopen byte makes the compiler define its own 1-byte symbol named dlopen. Without purego, the program either crashes with SIGSEGV when it calls it, or fails to link when other C code in the program also calls dlopen. It only appeared to work with purego linked in, because purego's own cgo package declares dlopen as a C import. Sorry for pointing in the wrong direction, and for leaving this without an update for two months.
goffi + other cgo packages (not purego): go build -ldflags=-linkmode=internal ./....
How to fix it. Every option we found has a cost, and one of them would add a few lines of C to goffi's CGO_ENABLED=1 builds. Before deciding, we've written up the full picture, the options we tested and their trade-offs here, and would appreciate input: #104
Activity
arturonaredo commentedon Aug 19, 2026
Is there any workaround to this issue?
Zoinen commentedon Aug 22, 2026
Downstream confirmation from f4: with
CGO_ENABLED=0, goffi v0.6.3 is pulled into the normal Linux amd64/arm64 builds throughinternal/fakecgo, so-extldflags '-static'does not produce a fully static artifact. The regular cross-builds succeed, while a-tags noffibuild removes the goffi/gogpu stack and can be static, but it also drops the gogpu, Wayland, and Ebiten backends.This is tracked downstream in unxed/f4#693. PR #73 appears to address the Linux CGO relocation path; f4's affected release path is the
!cgofakecgo path, so the remaining dynamic-loader dependency needs an upstream design/fix rather than a silent f4-only backend removal.— zoin-bot
kolkov commentedon Aug 23, 2026
Investigated this thoroughly. There are actually two separate problems here depending on CGO mode:
CGO_ENABLED=1 (the SDYNIMPORT error): The Go internal linker has a guard at
cmd/link/internal/ld/data.go:255that rejectsR_CALL → SDYNIMPORTrelocations on Linux. Darwin, AIX, Solaris, and OpenBSD are exempt — Linux isn't, even though the PLT machinery inamd64/asm.go:280already handles it correctly. The guard is redundant. OpenBSD was added to the exception list via #42671 for the same reason (pthreads migration neededcgo_import_dynamic). We're preparing an upstream Go proposal to add Linux.The immediate fix for goffi is to exclude the assembly JMP stubs (
dl_stubs_unix.s) fromcgobuilds via!cgobuild tag, and use//go:linknamevariable indirection instead — the external linker resolvesdlopen/dlsymdirectly from libc, so no SDYNIMPORT symbol exists in Go's link phase. This is exactly how purego handles it:dlfcn_stubs.shas!cgotag, andinternal/cgo/dlfcn_cgo_unix.gousesimport "C"for the cgo path. @lkmavi validated this approach in a Docker linux/arm64 environment.CGO_ENABLED=0 + purego in same binary: This is the existing fakecgo duplicate symbol issue (#22). Both goffi and purego define
_cgo_init,_cgo_thread_start, etc. — Go has nodupokfor data symbols. Workaround:-tags nofakecgodisables goffi's copy.Working on the
//go:linknamefix now. Will update when the PR is ready.kolkov commentedon Oct 17, 2026
Status update, and a correction to my comment from August.
Root cause. This is not specific to purego. With
CGO_ENABLED=1, goffi on Linux and FreeBSD fails to link whenever the final link is external: any package withimport "C"in the same program,-ldflags=-linkmode=external,-buildmode=c-sharedorc-archive. goffi reachesdlopen,dlsym,dlerrorand the errno accessor through assembly stubs that jump to//go:cgo_import_dynamicsymbols, andcmd/linkrejects relocations to such symbols when it links externally (the SDYNIMPORT check incmd/link/internal/ld/data.go; Darwin, OpenBSD, AIX and Solaris are exempt). goffi on its own links with the internal linker, which is why our CI never caught this. It has been this way sinceCGO_ENABLED=1support landed in v0.5.1.The
//go:linknameplan from my August comment does not work. We tested it://go:linkname libc_dlopen dlopenonvar libc_dlopen bytemakes the compiler define its own 1-byte symbol nameddlopen. Without purego, the program either crashes with SIGSEGV when it calls it, or fails to link when other C code in the program also callsdlopen. It only appeared to work with purego linked in, because purego's own cgo package declaresdlopenas a C import. Sorry for pointing in the wrong direction, and for leaving this without an update for two months.Workarounds for now
CGO_ENABLED=0 go build -tags nofakecgo ./...(the link: duplicated definition of symbol _cgo_init #22 workaround).go build -ldflags=-linkmode=internal ./....How to fix it. Every option we found has a cost, and one of them would add a few lines of C to goffi's
CGO_ENABLED=1builds. Before deciding, we've written up the full picture, the options we tested and their trade-offs here, and would appreciate input: #104docs: bring documentation in line with the code
docs: bring documentation in line with the code