Skip to content

FreeBSD/NetBSD fakecgo hardcodes "libpthread.so" via go:cgo_import_dynamic, which doesn't exist on FreeBSD 15+ #480

Description

@rcina

PureGo Version

purego v0.10.1 (as vendored by wandb v0.28.0)

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • NetBSD
  • Android
  • iOS

Go Version (go version)

Go 1.26.5 (go version go1.26.5 freebsd/amd64), lang/go126 FreeBSD port

What steps will reproduce the problem?

Repository: ebitengine/purego

Description:
On FreeBSD 15.1-STABLE (amd64), Go binaries built with CGO_ENABLED=0 that
use purego (via its fakecgo mechanism) produce an ELF binary with an
unresolvable NEEDED entry for libpthread.so, which does not exist
anywhere in the FreeBSD 15 base system.

Root cause (verified in source):
internal/fakecgo/zsymbols_freebsd.go hardcodes the dynamic import target
"libpthread.so" via go:cgo_import_dynamic directives, e.g.:

//go:cgo_import_dynamic purego_pthread_create pthread_create "libpthread.so"
//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libpthread.so"
(and 8 more pthread_* symbols in the same file, lines 19-28)

The same hardcoded "libpthread.so" string also appears in
internal/fakecgo/zsymbols_netbsd.go (lines 20-29). By contrast,
internal/fakecgo/zsymbols_linux.go uses "libpthread.so.0" (lines 19-28),
a versioned name that still exists on Linux.

Confirmed real-world impact:
wandb (github.com/wandb/wandb v0.28.0) vendors purego v0.10.1 and uses it
to dlopen a companion Rust shared library on non-cgo builds. The resulting
wandb-core binary (built with CGO_ENABLED=0, confirmed via wandb's own
build script core/hatch.py) has:

NEEDED libpthread.so
NEEDED libc.so.7

with undefined symbols pthread_create, pthread_attr_init, pthread_detach,
pthread_sigmask, pthread_mutex_lock, pthread_mutex_unlock,
pthread_cond_broadcast, pthread_setspecific, pthread_attr_getstacksize,
pthread_attr_destroy (confirmed via objdump -T).

Notably, other binaries built into the same package via Rust/cargo
(librust_parquet_ffi.so, wandb-xpu) correctly link against libthr.so.3 —
the current FreeBSD threading library — rather than libpthread.so,
confirmed via readelf -d.

Historically, FreeBSD's /usr/lib/libpthread.so was maintained only as a
compatibility symlink to libthr.so. This symlink is absent on FreeBSD
15.1-STABLE (confirmed: ls /lib/libpthread* /usr/lib/libpthread*
returns no matches; libthr.so.3 is present).

Steps to reproduce:

  1. On FreeBSD 15.1-STABLE (amd64), build any Go program that imports
    purego with CGO_ENABLED=0, GOOS=freebsd, GOARCH=amd64.
  2. readelf -d | grep NEEDED
  3. Observe NEEDED libpthread.so.
  4. Confirm the symlink does not exist in base:
    ls /lib/libpthread* /usr/lib/libpthread* → no matches
    ls /usr/lib/libthr.so* → present

Possible fix:
On FreeBSD, updating the go:cgo_import_dynamic directives in
internal/fakecgo/zsymbols_freebsd.go to target "libthr.so.3" instead of
"libpthread.so" would likely resolve this, consistent with how Rust/cargo
currently resolves these same symbols on this system (see the wandb
example above). I haven't verified this is the right replacement across
all supported FreeBSD versions/architectures, or whether NetBSD's
zsymbols_netbsd.go needs the same or a different fix — that's for the
maintainers to judge, since I'm not familiar enough with either platform's
library layout to say confidently. Flagging both files since they share
the same hardcoded string.

Environment:
FreeBSD 15.1-STABLE amd64 (stable/15-n284336-034e21efa19d, GENERIC kernel)
purego v0.10.1 (as vendored by wandb v0.28.0)
Go 1.26 (lang/go126 FreeBSD port)

What is the expected result?

A Go binary built with CGO_ENABLED=0 using purego's FreeBSD fakecgo path
should not have a NEEDED entry for a shared library that doesn't exist in
FreeBSD 15+ base. The go:cgo_import_dynamic directives in
internal/fakecgo/zsymbols_freebsd.go should target a library that
actually exists on the target FreeBSD version (e.g. libthr.so.3, as
Rust/cargo already does for the same pthread symbols on this system), so
the resulting binary's dependencies fully resolve without any manual
workaround.

What happens instead?

The resulting binary has an unresolvable NEEDED entry for libpthread.so,
which does not exist anywhere in FreeBSD 15.1-STABLE base. This causes
build/QA tooling that validates shared library dependencies (e.g.
FreeBSD's poudriere ports build system) to flag the binary as broken and
rebuild it on every run, since no dependency can satisfy the missing
library. Confirmed in a real binary (wandb-core, built from wandb v0.28.0
vendoring purego v0.10.1): readelf -d shows NEEDED libpthread.so, and
objdump -T shows undefined symbols pthread_create, pthread_attr_init,
pthread_detach, pthread_sigmask, pthread_mutex_lock, pthread_mutex_unlock,
pthread_cond_broadcast, pthread_setspecific, pthread_attr_getstacksize,
pthread_attr_destroy — all traced to the hardcoded "libpthread.so" string
in zsymbols_freebsd.go lines 19-28.

Anything else you feel useful to add?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions