From 6772a59ede410a0186650ef4e545f1aceddf2f32 Mon Sep 17 00:00:00 2001 From: Arturo Naredo Date: Thu, 20 Aug 2026 08:40:07 +0200 Subject: [PATCH 1/2] fix(linux): use cgo bridge for dynamic system symbols 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 #72: clr-aistudioinstaller/cmd/cli now links on Linux amd64 and arm64 when replaced with this checkout. Fixes #72 --- internal/cbridge/linux.go | 77 ++++++++++++++++++++++++++++ internal/cbridge/linux_test.go | 54 +++++++++++++++++++ internal/dl/dl_linux.go | 5 +- internal/dl/dl_linux_cgo.go | 57 ++++++++++++++++++++ internal/dl/dl_stubs_arm64.s | 6 +-- internal/dl/dl_stubs_unix.s | 6 +-- internal/dl/dl_unix.go | 2 +- internal/syscall/errno_linux.go | 2 +- internal/syscall/errno_linux_cgo.go | 10 ++++ internal/syscall/errno_stubs_amd64.s | 2 +- internal/syscall/errno_stubs_arm64.s | 2 +- internal/syscall/errno_unix.go | 2 +- 12 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 internal/cbridge/linux.go create mode 100644 internal/cbridge/linux_test.go create mode 100644 internal/dl/dl_linux_cgo.go create mode 100644 internal/syscall/errno_linux_cgo.go diff --git a/internal/cbridge/linux.go b/internal/cbridge/linux.go new file mode 100644 index 0000000..6796426 --- /dev/null +++ b/internal/cbridge/linux.go @@ -0,0 +1,77 @@ +//go:build linux && !android && cgo + +package cbridge + +/* +#cgo LDFLAGS: -ldl +#include +#include +#include +#include + +typedef struct { + void* value; + const char* error; +} goffi_dl_result; + +static void goffi_dlopen(const char* path, int mode, goffi_dl_result* result) { + result->value = dlopen(path, mode); + result->error = result->value == NULL ? dlerror() : NULL; +} + +static void goffi_dlsym(uintptr_t handle, const char* name, goffi_dl_result* result) { + dlerror(); + result->value = dlsym((void*)handle, name); + result->error = result->value == NULL ? dlerror() : NULL; +} + +static void* goffi_errno_location_address(void) { + return (void*)&__errno_location; +} +*/ +import "C" + +import "unsafe" + +// Dlopen loads a shared library and captures the loader error, if any. +func Dlopen(path string, mode int) (uintptr, string) { + cpath := C.CString(path) + defer C.free(unsafe.Pointer(cpath)) + + var result C.goffi_dl_result + C.goffi_dlopen(cpath, C.int(mode), &result) + if result.value == nil { + return 0, dlerrorString(result.error) + } + return uintptr(result.value), "" +} + +// Dlsym resolves a symbol and captures the loader error, if any. +func Dlsym(handle uintptr, name string) (uintptr, string) { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + + var result C.goffi_dl_result + C.goffi_dlsym(C.uintptr_t(handle), cname, &result) + if result.value == nil { + return 0, dlerrorString(result.error) + } + return uintptr(result.value), "" +} + +// Dlerror returns the current thread's dynamic-loader error. +func Dlerror() string { + return dlerrorString(C.dlerror()) +} + +func dlerrorString(message *C.char) string { + if message == nil { + return "" + } + return C.GoString(message) +} + +// ErrnoLocationAddress returns the address of libc's errno accessor. +func ErrnoLocationAddress() uintptr { + return uintptr(C.goffi_errno_location_address()) +} diff --git a/internal/cbridge/linux_test.go b/internal/cbridge/linux_test.go new file mode 100644 index 0000000..2c633e4 --- /dev/null +++ b/internal/cbridge/linux_test.go @@ -0,0 +1,54 @@ +//go:build linux && !android && cgo + +package cbridge + +import "testing" + +const rtldNow = 0x00002 + +func openStandardCLibrary(t *testing.T) uintptr { + t.Helper() + + candidates := []string{ + "libc.so.6", + "libdl.so.2", + "libc.musl-x86_64.so.1", + "libc.musl-aarch64.so.1", + } + for _, candidate := range candidates { + if handle, _ := Dlopen(candidate, rtldNow); handle != 0 { + return handle + } + } + + t.Skipf("no standard libc or libdl candidate available; tried %v", candidates) + return 0 +} + +func TestDlopenAndDlsym(t *testing.T) { + handle := openStandardCLibrary(t) + + symbol, errMessage := Dlsym(handle, "malloc") + if symbol == 0 { + t.Fatalf("Dlsym(malloc) returned 0: %s", errMessage) + } + if errMessage != "" { + t.Fatalf("Dlsym(malloc) returned an unexpected error: %s", errMessage) + } +} + +func TestDlopenInvalidLibrary(t *testing.T) { + handle, errMessage := Dlopen("libgoffi-does-not-exist.so", rtldNow) + if handle != 0 { + t.Fatalf("Dlopen returned handle %#x for an invalid library", handle) + } + if errMessage == "" { + t.Fatal("Dlopen returned an empty error for an invalid library") + } +} + +func TestErrnoLocationAddress(t *testing.T) { + if address := ErrnoLocationAddress(); address == 0 { + t.Fatal("ErrnoLocationAddress returned 0") + } +} diff --git a/internal/dl/dl_linux.go b/internal/dl/dl_linux.go index bec6a9b..1939ae3 100644 --- a/internal/dl/dl_linux.go +++ b/internal/dl/dl_linux.go @@ -1,4 +1,4 @@ -//go:build linux && !android +//go:build linux && !android && !cgo // Linux-specific constants for dynamic library loading. // @@ -10,8 +10,7 @@ package dl // Link to libdl.so.2 functions using cgo_import_dynamic. -// This works under both CGO_ENABLED=0 (where fakecgo provides the cgo runtime) -// and CGO_ENABLED=1 (where the standard runtime/cgo is linked, see cgo.go). +// This is used with CGO_ENABLED=0, where fakecgo provides the cgo runtime. // // Note on glibc >= 2.34: libdl.so.2 is a stub (an empty .so with a versioned // symlink to libc.so.6). dlopen/dlsym/dlerror/dlclose all live in libc.so.6 diff --git a/internal/dl/dl_linux_cgo.go b/internal/dl/dl_linux_cgo.go new file mode 100644 index 0000000..7d67d52 --- /dev/null +++ b/internal/dl/dl_linux_cgo.go @@ -0,0 +1,57 @@ +//go:build linux && !android && cgo + +package dl + +import ( + "fmt" + + "github.com/go-webgpu/goffi/internal/cbridge" +) + +// RTLD constants from for dynamic library loading on Linux. +const ( + // RTLD_LAZY performs relocations at an implementation-dependent time. + RTLD_LAZY = 0x00001 + + // RTLD_NOW resolves all symbols when loading the library (recommended). + RTLD_NOW = 0x00002 + + // RTLD_GLOBAL makes all symbols available for relocation processing of other modules. + // NOTE: Different from macOS (0x8) - Linux uses 0x00100 + RTLD_GLOBAL = 0x00100 + + // RTLD_LOCAL makes symbols not available for relocation processing by other modules. + RTLD_LOCAL = 0x00000 +) + +// RTLD_DEFAULT is a pseudo-handle for dlsym to search for any loaded symbol. +// NOTE: Different from macOS (1<<64 - 2) - Linux uses 0 +const RTLD_DEFAULT = 0x00000 + +// Dlopen loads a shared library. +func Dlopen(path string, mode int) (uintptr, error) { + handle, errMessage := cbridge.Dlopen(path, mode) + if handle == 0 { + if errMessage == "" { + errMessage = "unknown error" + } + return 0, fmt.Errorf("dlopen failed: %s", errMessage) + } + return handle, nil +} + +// Dlsym returns the address of a symbol in a loaded library. +func Dlsym(handle uintptr, name string) (uintptr, error) { + symbol, errMessage := cbridge.Dlsym(handle, name) + if symbol == 0 { + if errMessage == "" { + errMessage = "unknown error" + } + return 0, fmt.Errorf("dlsym failed: %s", errMessage) + } + return symbol, nil +} + +// Dlclose intentionally retains process-lifetime mappings, matching the +// existing implementation's semantics. +func Dlclose(uintptr) error { return nil } diff --git a/internal/dl/dl_stubs_arm64.s b/internal/dl/dl_stubs_arm64.s index cec205d..c9194f6 100644 --- a/internal/dl/dl_stubs_arm64.s +++ b/internal/dl/dl_stubs_arm64.s @@ -1,11 +1,11 @@ -//go:build ((linux && !android) || darwin || freebsd) && arm64 +//go:build ((linux && !android && !cgo) || darwin || freebsd) && arm64 #include "textflag.h" // JMP stubs to dynamically linked symbols (ARM64) // These symbols are linked via //go:cgo_import_dynamic in: -// - dl_linux_nocgo.go (Linux: libdl.so.2) -// - dl_darwin_nocgo.go (macOS: libSystem.B.dylib) +// - dl_linux.go (Linux: libdl.so.2) +// - dl_darwin.go (macOS: libSystem.B.dylib) // dlopen_stub: B to dlopen TEXT dlopen_stub(SB), NOSPLIT|NOFRAME, $0-0 diff --git a/internal/dl/dl_stubs_unix.s b/internal/dl/dl_stubs_unix.s index 03cbf65..99eda59 100644 --- a/internal/dl/dl_stubs_unix.s +++ b/internal/dl/dl_stubs_unix.s @@ -1,11 +1,11 @@ -//go:build (linux || darwin || freebsd) && amd64 +//go:build ((linux && !android && !cgo) || darwin || freebsd) && amd64 #include "textflag.h" // JMP stubs to dynamically linked symbols // These symbols are linked via //go:cgo_import_dynamic in: -// - dl_linux_nocgo.go (Linux: libdl.so.2) -// - dl_darwin_nocgo.go (macOS: libSystem.B.dylib) +// - dl_linux.go (Linux: libdl.so.2) +// - dl_darwin.go (macOS: libSystem.B.dylib) // dlopen_stub: JMP to dlopen TEXT dlopen_stub(SB), NOSPLIT|NOFRAME, $0-0 diff --git a/internal/dl/dl_unix.go b/internal/dl/dl_unix.go index 7299aed..2172d66 100644 --- a/internal/dl/dl_unix.go +++ b/internal/dl/dl_unix.go @@ -1,4 +1,4 @@ -//go:build (linux && !android) || darwin || freebsd +//go:build (linux && !android && !cgo) || darwin || freebsd // OUR OWN Dlopen/Dlsym implementation - NO dependencies! // Uses runtime.cgocall approach similar to syscall6. diff --git a/internal/syscall/errno_linux.go b/internal/syscall/errno_linux.go index 5836294..95546c6 100644 --- a/internal/syscall/errno_linux.go +++ b/internal/syscall/errno_linux.go @@ -1,4 +1,4 @@ -//go:build linux && !android && (amd64 || arm64) +//go:build linux && !android && !cgo && (amd64 || arm64) package syscall diff --git a/internal/syscall/errno_linux_cgo.go b/internal/syscall/errno_linux_cgo.go new file mode 100644 index 0000000..f490beb --- /dev/null +++ b/internal/syscall/errno_linux_cgo.go @@ -0,0 +1,10 @@ +//go:build linux && !android && cgo && (amd64 || arm64) + +package syscall + +import "github.com/go-webgpu/goffi/internal/cbridge" + +// ErrnoFnAddr returns the address of glibc's errno-location function. +func ErrnoFnAddr() uintptr { + return cbridge.ErrnoLocationAddress() +} diff --git a/internal/syscall/errno_stubs_amd64.s b/internal/syscall/errno_stubs_amd64.s index bf45d73..3739c3d 100644 --- a/internal/syscall/errno_stubs_amd64.s +++ b/internal/syscall/errno_stubs_amd64.s @@ -1,4 +1,4 @@ -//go:build ((linux && !android) || darwin || freebsd) && amd64 +//go:build ((linux && !android && !cgo) || darwin || freebsd) && amd64 #include "textflag.h" diff --git a/internal/syscall/errno_stubs_arm64.s b/internal/syscall/errno_stubs_arm64.s index 24f9ba8..2d8baf6 100644 --- a/internal/syscall/errno_stubs_arm64.s +++ b/internal/syscall/errno_stubs_arm64.s @@ -1,4 +1,4 @@ -//go:build ((linux && !android) || (android && !cgo) || darwin || freebsd) && arm64 +//go:build ((linux && !android && !cgo) || (android && !cgo) || darwin || freebsd) && arm64 #include "textflag.h" diff --git a/internal/syscall/errno_unix.go b/internal/syscall/errno_unix.go index f815301..b89dbdb 100644 --- a/internal/syscall/errno_unix.go +++ b/internal/syscall/errno_unix.go @@ -1,4 +1,4 @@ -//go:build ((linux && !android) || (android && !cgo) || darwin || freebsd) && (amd64 || arm64) +//go:build ((linux && !android && !cgo) || (android && !cgo) || darwin || freebsd) && (amd64 || arm64) package syscall From 5c495e6cd301bf5a6251d41cfac418420ab56c7e Mon Sep 17 00:00:00 2001 From: Arturo Naredo Date: Fri, 21 Aug 2026 13:13:49 +0200 Subject: [PATCH 2/2] refactor(linux): deduplicate RTLD_* constants and remove dead cbridge.Dlerror Addresses the three minor nits flagged in the #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 #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. --- internal/cbridge/linux.go | 5 ----- internal/dl/dl_linux.go | 28 +++++---------------------- internal/dl/dl_linux_cgo.go | 20 +------------------ internal/dl/dl_linux_consts.go | 35 ++++++++++++++++++++++++++++++++++ internal/dl/dl_unix.go | 9 ++++++--- 5 files changed, 47 insertions(+), 50 deletions(-) create mode 100644 internal/dl/dl_linux_consts.go diff --git a/internal/cbridge/linux.go b/internal/cbridge/linux.go index 6796426..39ee08c 100644 --- a/internal/cbridge/linux.go +++ b/internal/cbridge/linux.go @@ -59,11 +59,6 @@ func Dlsym(handle uintptr, name string) (uintptr, string) { return uintptr(result.value), "" } -// Dlerror returns the current thread's dynamic-loader error. -func Dlerror() string { - return dlerrorString(C.dlerror()) -} - func dlerrorString(message *C.char) string { if message == nil { return "" diff --git a/internal/dl/dl_linux.go b/internal/dl/dl_linux.go index 1939ae3..b27cdb7 100644 --- a/internal/dl/dl_linux.go +++ b/internal/dl/dl_linux.go @@ -1,9 +1,11 @@ //go:build linux && !android && !cgo -// Linux-specific constants for dynamic library loading. +// Linux no-cgo implementation: link to libdl.so.2 functions via +// cgo_import_dynamic. The shared RTLD_* constants live in dl_linux_consts.go +// (compiled for both the cgo and !cgo paths). // -// These constants differ from macOS values but the dlopen/dlsym API is identical -// (POSIX standardized). The calling convention is System V AMD64 ABI on both platforms. +// The dlopen/dlsym API is POSIX-standardized; the calling convention is +// System V AMD64 ABI. // // Reference: https://codebrowser.dev/glibc/glibc/bits/dlfcn.h.html @@ -28,23 +30,3 @@ package dl // Force dependency on libdl.so.2 //go:cgo_import_dynamic _ _ "libdl.so.2" - -// RTLD constants from for dynamic library loading on Linux. -const ( - // RTLD_LAZY performs relocations at an implementation-dependent time. - RTLD_LAZY = 0x00001 - - // RTLD_NOW resolves all symbols when loading the library (recommended). - RTLD_NOW = 0x00002 - - // RTLD_GLOBAL makes all symbols available for relocation processing of other modules. - // NOTE: Different from macOS (0x8) - Linux uses 0x00100 - RTLD_GLOBAL = 0x00100 - - // RTLD_LOCAL makes symbols not available for relocation processing by other modules. - RTLD_LOCAL = 0x00000 -) - -// RTLD_DEFAULT is a pseudo-handle for dlsym to search for any loaded symbol. -// NOTE: Different from macOS (1<<64 - 2) - Linux uses 0 -const RTLD_DEFAULT = 0x00000 diff --git a/internal/dl/dl_linux_cgo.go b/internal/dl/dl_linux_cgo.go index 7d67d52..c00bacc 100644 --- a/internal/dl/dl_linux_cgo.go +++ b/internal/dl/dl_linux_cgo.go @@ -8,25 +8,7 @@ import ( "github.com/go-webgpu/goffi/internal/cbridge" ) -// RTLD constants from for dynamic library loading on Linux. -const ( - // RTLD_LAZY performs relocations at an implementation-dependent time. - RTLD_LAZY = 0x00001 - - // RTLD_NOW resolves all symbols when loading the library (recommended). - RTLD_NOW = 0x00002 - - // RTLD_GLOBAL makes all symbols available for relocation processing of other modules. - // NOTE: Different from macOS (0x8) - Linux uses 0x00100 - RTLD_GLOBAL = 0x00100 - - // RTLD_LOCAL makes symbols not available for relocation processing by other modules. - RTLD_LOCAL = 0x00000 -) - -// RTLD_DEFAULT is a pseudo-handle for dlsym to search for any loaded symbol. -// NOTE: Different from macOS (1<<64 - 2) - Linux uses 0 -const RTLD_DEFAULT = 0x00000 +// RTLD_* constants are shared with the !cgo path; see dl_linux_consts.go. // Dlopen loads a shared library. func Dlopen(path string, mode int) (uintptr, error) { diff --git a/internal/dl/dl_linux_consts.go b/internal/dl/dl_linux_consts.go new file mode 100644 index 0000000..c14c83e --- /dev/null +++ b/internal/dl/dl_linux_consts.go @@ -0,0 +1,35 @@ +//go:build linux && !android + +// Linux-specific constants for dynamic library loading. +// +// Shared between the no-cgo path (dl_linux.go, which declares the +// cgo_import_dynamic directives) and the cgo path (dl_linux_cgo.go, which +// routes through internal/cbridge). The values are identical across both +// paths; only the resolver mechanism differs. +// +// Calling convention is System V AMD64 ABI on Linux. The dlopen/dlsym API is +// POSIX-standardized. +// +// Reference: https://codebrowser.dev/glibc/glibc/bits/dlfcn.h.html + +package dl + +// RTLD constants from for dynamic library loading on Linux. +const ( + // RTLD_LAZY performs relocations at an implementation-dependent time. + RTLD_LAZY = 0x00001 + + // RTLD_NOW resolves all symbols when loading the library (recommended). + RTLD_NOW = 0x00002 + + // RTLD_GLOBAL makes all symbols available for relocation processing of other modules. + // NOTE: Different from macOS (0x8) - Linux uses 0x00100 + RTLD_GLOBAL = 0x00100 + + // RTLD_LOCAL makes symbols not available for relocation processing by other modules. + RTLD_LOCAL = 0x00000 +) + +// RTLD_DEFAULT is a pseudo-handle for dlsym to search for any loaded symbol. +// NOTE: Different from macOS (1<<64 - 2) - Linux uses 0 +const RTLD_DEFAULT = 0x00000 diff --git a/internal/dl/dl_unix.go b/internal/dl/dl_unix.go index 2172d66..72ae4dc 100644 --- a/internal/dl/dl_unix.go +++ b/internal/dl/dl_unix.go @@ -6,11 +6,13 @@ // This implementation uses System V AMD64 ABI calling convention, which is // IDENTICAL on Linux and macOS. Platform-specific constants (RTLD_*) are // defined in: -// - dl_linux.go (Linux-specific constants) +// - dl_linux_consts.go (Linux-specific constants, shared by cgo and !cgo) // - dl_darwin.go (macOS-specific constants) +// - dl_freebsd.go (FreeBSD-specific constants) // // The assembly wrappers (dl_wrappers_unix.s, dl_stubs_unix.s) and the -// core logic here work identically on both platforms. +// core logic here work identically on Linux, macOS, and FreeBSD (their +// paths; cgo is handled separately in dl_linux_cgo.go). package dl @@ -20,7 +22,8 @@ import ( "unsafe" ) -// RTLD constants are platform-specific - see dl_linux.go and dl_darwin.go +// RTLD constants are platform-specific - see dl_linux_consts.go, +// dl_darwin.go and dl_freebsd.go. //go:linkname runtime_cgocall runtime.cgocall //go:noescape