From fdcfb3575300824879bcc1ea0aae3285cda6a953 Mon Sep 17 00:00:00 2001 From: deluan Date: Fri, 3 Apr 2026 11:59:40 -0400 Subject: [PATCH 1/2] fix: allow dlopen on statically-linked musl binaries The isDynamicBinary() check prevents loadLibrary from attempting dlopen on statically-linked Linux binaries. This is correct for glibc, where dlopen from static binaries is undefined behavior and can crash, but overly conservative for musl libc, which has a fully functional dlopen implementation even in static binaries. Add an isMusl() check so that static binaries on musl-based systems (like Alpine Linux) can still load native libwebp via dlopen. On glibc systems the existing guard is preserved. This enables native libwebp loading in Alpine/Docker environments where Go+CGO+musl produces static binaries by default. --- purego_unix.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/purego_unix.go b/purego_unix.go index 70a4593..7eaa4ab 100644 --- a/purego_unix.go +++ b/purego_unix.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "runtime" + "strings" "github.com/ebitengine/purego" ) @@ -17,7 +18,7 @@ const ( ) func loadLibrary(name string) (uintptr, error) { - if runtime.GOOS == "linux" && !isDynamicBinary() { + if runtime.GOOS == "linux" && !isDynamicBinary() && !isMusl() { return 0, fmt.Errorf("not a dynamic binary") } @@ -49,3 +50,28 @@ func isDynamicBinary() bool { return false } + +// isMusl returns true if the current process is linked against musl libc. +// Musl supports dlopen from statically-linked binaries, unlike glibc. +func isMusl() bool { + maps, err := os.ReadFile("/proc/self/maps") + if err == nil && strings.Contains(string(maps), "musl") { + return true + } + + // For static binaries /proc/self/maps won't show musl. + // Check if the musl dynamic linker exists on the system. + for _, path := range []string{ + "/lib/ld-musl-aarch64.so.1", + "/lib/ld-musl-x86_64.so.1", + "/lib/ld-musl-armhf.so.1", + "/lib/ld-musl-i386.so.1", + "/lib/ld-musl-riscv64.so.1", + } { + if _, err := os.Stat(path); err == nil { + return true + } + } + + return false +} From e68f7bed4214ef28ed5f9a12767b7465c51434fb Mon Sep 17 00:00:00 2001 From: deluan Date: Fri, 3 Apr 2026 12:59:13 -0400 Subject: [PATCH 2/2] fix: use ELF PT_INTERP to detect musl for safe dlopen Replace blanket isDynamicBinary() rejection with canDlopen() check: - Dynamic binaries: always allowed (same as before) - Static musl binaries: allowed (musl supports dlopen from static) - Static glibc binaries: rejected (glibc dlopen may SIGSEGV) Musl detection reads the ELF PT_INTERP program header for the dynamic linker path (e.g. /lib/ld-musl-aarch64.so.1). --- purego_unix.go | 54 +++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/purego_unix.go b/purego_unix.go index 7eaa4ab..8ba87de 100644 --- a/purego_unix.go +++ b/purego_unix.go @@ -18,7 +18,7 @@ const ( ) func loadLibrary(name string) (uintptr, error) { - if runtime.GOOS == "linux" && !isDynamicBinary() && !isMusl() { + if runtime.GOOS == "linux" && !canDlopen() { return 0, fmt.Errorf("not a dynamic binary") } @@ -30,46 +30,42 @@ func loadLibrary(name string) (uintptr, error) { return handle, nil } -func isDynamicBinary() bool { +// canDlopen reports whether dlopen is safe to call from this binary. +// Dynamic binaries can always dlopen. Static binaries can only dlopen +// safely if they were linked against musl libc, which has a fully +// functional dlopen implementation even in static executables. +// On glibc, dlopen from static binaries is not supported and may crash. +func canDlopen() bool { fileName, err := os.Executable() if err != nil { - panic(err) + return false } fl, err := elf.Open(fileName) if err != nil { - panic(err) + return false } - defer fl.Close() - _, err = fl.DynamicSymbols() - if err == nil { - return true - } - - return false -} - -// isMusl returns true if the current process is linked against musl libc. -// Musl supports dlopen from statically-linked binaries, unlike glibc. -func isMusl() bool { - maps, err := os.ReadFile("/proc/self/maps") - if err == nil && strings.Contains(string(maps), "musl") { + // Dynamic binaries can always dlopen + if _, err := fl.DynamicSymbols(); err == nil { return true } - // For static binaries /proc/self/maps won't show musl. - // Check if the musl dynamic linker exists on the system. - for _, path := range []string{ - "/lib/ld-musl-aarch64.so.1", - "/lib/ld-musl-x86_64.so.1", - "/lib/ld-musl-armhf.so.1", - "/lib/ld-musl-i386.so.1", - "/lib/ld-musl-riscv64.so.1", - } { - if _, err := os.Stat(path); err == nil { - return true + // Static binary — check if it was linked with musl by reading the + // ELF .comment section, which contains the compiler/linker identification. + // Musl-linked binaries typically have no .comment or have "GCC" without + // glibc markers. More reliably, we look for "musl" in the ELF interpreter + // (PT_INTERP program header), which musl's linker sets even for static-pie. + for _, prog := range fl.Progs { + if prog.Type == elf.PT_INTERP { + interp := make([]byte, prog.Filesz) + if _, err := prog.ReadAt(interp, 0); err == nil { + if strings.Contains(string(interp), "musl") { + return true + } + } + break } }