Skip to content

unhandled relocation ... SDYNIMPORT when goffi and ebitengine/purego are linked into the same binary #72

Open

Description

@unxed

Environment

go1.25.5 linux/amd64, Ubuntu 24.04.4, CGO_ENABLED=0 and 1 — both fail
goffi v0.6.3 (also v0.6.2), ebitengine/purego v0.11.0-alpha.8

Repro

Minimal module attached (main.go + go.mod + go.sum). ffi.LoadLibrary alone
builds fine; purego.Dlopen alone builds fine; together in one binary:

go build .
go test ./...

fail with:

dlopen_stub: unhandled relocation for goffi_dlopen (type 65 (SDYNIMPORT) rtype 7 (R_CALL))
dlerror_stub: unhandled relocation for goffi_dlerror (type 65 (SDYNIMPORT) rtype 7 (R_CALL))

Anything else useful

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.

goffi-repro.tar.gz

Activity

  1. arturonaredo commented on Aug 19, 2026

    @arturonaredo

    Is there any workaround to this issue?

  2. Zoinen commented on Aug 22, 2026

    @Zoinen

    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.

    — zoin-bot

  3. kolkov commented on Aug 23, 2026

    @kolkov
    Contributor

    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.

  4. added this to the v0.6.7 milestone on Oct 17, 2026
  5. kolkov commented on Oct 17, 2026

    @kolkov
    Contributor

    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.

    Workarounds for now

    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

  6. modified the milestones: v0.6.7, v0.6.8 on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions