Skip to content

compileopts,builder: add -libc=glibc for linking system shared libraries - #5838

Open
davecheney wants to merge 2 commits into
tinygo-org:devfrom
davecheney:glibc-libc
Open

davecheney wants to merge 2 commits into
tinygo-org:devfrom
davecheney:glibc-libc

Conversation

@davecheney

Copy link
Copy Markdown
Contributor

TinyGo links Linux binaries against the musl it ships, statically. That is a good default: it is the reason a build behaves the same on any machine and produces a single file. It also makes it impossible to link against a system shared library.

Any shared library on a glibc distribution drags glibc in behind it, and two libcs cannot share a process. Whichever installs the thread pointer first owns it, and the other one's TLS accesses then read the wrong memory. Linking SDL and calling SDL_Init segfaults inside glibc's calloc, reading its malloc tcache through musl's TCB.

There is a second, earlier failure hiding behind that one. lld only emits PT_INTERP when told --dynamic-linker, which nothing does here, so a binary that pulls in a shared library names no interpreter at all: it loads with its PLT unresolved and the first call through it lands on a null GOT entry. That one looks like a bug but is really the same story, since it is only reachable by linking a shared library in the first place.

So add the other half: -libc=glibc uses the glibc already on the machine, linked dynamically, the way darwin-libSystem and mingw-w64 already use the system libc on their platforms. Linux was the odd one out.

Tested on linux/amd64: the default build is still statically linked musl; -libc=glibc builds and runs both a pure Go program and a cgo program that calls into SDL2; the picolibc board targets are unaffected.

@davecheney

Copy link
Copy Markdown
Contributor Author

Another libc? what's wrong with musl?

Factually, nothing, but practically if you are trying to link against something like SDL2/3 then glibc is rusted on. Therefore this PR adds the option to switch from musl to glibc on linux systems specifically to link against bigass frameworks like SDL. The upside from taking this patch is there is a viable path to building linux + sdl apps using tinygo.

@dgryski dgryski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but I haven't tested it. We should probably add a small makefile test target to make sure this keeps working.

@davecheney

Copy link
Copy Markdown
Contributor Author

LGTM but I haven't tested it. We should probably add a small makefile test target to make sure this keeps working.

I'll gin up a SDL example as this is the proof in the pudding

@deadprogram deadprogram added this to the 0.44 milestone Oct 9, 2026
@deadprogram

Copy link
Copy Markdown
Member

LGTM but I haven't tested it. We should probably add a small makefile test target to make sure this keeps working.

I'll gin up a SDL example as this is the proof in the pudding

That also sounds good, but we should have at least minimal test coverage here, IMO.

@davecheney

Copy link
Copy Markdown
Contributor Author

LGTM but I haven't tested it. We should probably add a small makefile test target to make sure this keeps working.

I'll gin up a SDL example as this is the proof in the pudding

That also sounds good, but we should have at least minimal test coverage here, IMO.

Leave it with me

TinyGo links Linux binaries against the musl it ships, statically. That is
a good default: it is the reason a build behaves the same on any machine
and produces a single file. It also makes it impossible to link against a
system shared library.

Any shared library on a glibc distribution drags glibc in behind it, and
two libcs cannot share a process. Whichever installs the thread pointer
first owns it, and the other one's TLS accesses then read the wrong
memory. Linking SDL and calling SDL_Init segfaults inside glibc's calloc,
reading its malloc tcache through musl's TCB.

There is a second, earlier failure hiding behind that one. lld only emits
PT_INTERP when told --dynamic-linker, which nothing does here, so a binary
that pulls in a shared library names no interpreter at all: it loads with
its PLT unresolved and the first call through it lands on a null GOT
entry. That one looks like a bug but is really the same story, since it is
only reachable by linking a shared library in the first place.

So add the other half: -libc=glibc uses the glibc already on the machine,
linked dynamically, the way darwin-libSystem and mingw-w64 already use the
system libc on their platforms. Linux was the odd one out.

Tested on linux/amd64: the default build is still statically linked musl;
-libc=glibc builds and runs both a pure Go program and a cgo program that
calls into SDL2; the picolibc board targets are unaffected.
Add a small cgo program that links against the system glibc and runs it
from TestBuild on linux hosts. It calls gnu_get_libc_version, which only
glibc has, and it uses malloc and errno on the main thread and on a
goroutine. These use glibc's TLS, so the program only runs if the binary
names the dynamic linker and glibc owns the thread pointer.

The test skips if the glibc development files are not installed.
@davecheney

Copy link
Copy Markdown
Contributor Author

@deadprogram PTAL. I added a glibc test to the linux-assert build.

@deadprogram

Copy link
Copy Markdown
Member

Thanks @davecheney for working on this. Being able to link SDL on Linux will be very useful. Here are a few notes, edited from an automated review.

  1. On my Ubuntu 24.04 machine with GCC 13 and 14 installed, tinygo build -libc=glibc ./testdata/glibc fails with exit code 1. The only output is this clang warning:

    warning: future releases of the clang compiler will prefer GCC installations containing libstdc++ include directories; '/../lib/gcc/x86_64-linux-gnu/13' would be chosen over '/../lib/gcc/x86_64-linux-gnu/14'
    

    With the -gnu triple, clang goes looking for a GCC install, and cgo reports every libclang diagnostic as an error (cgo/libclang.go line 155). When I returned -Wno-gcc-install-dir-libstdcxx from LibcCFlags for glibc, the test built and the output matched out.txt. The binary was dynamically linked with /lib64/ld-linux-x86-64.so.2 as its interpreter. That flag may not be the best fix, but some way to keep host GCC detection from breaking cgo might be needed. CI probably passes because the runner has a single GCC.

  2. GlibcLibDir checks the same directories in the same order for every GOARCH. On Fedora with the 32-bit glibc-devel installed, /usr/lib/crt1.o is the i686 one, so it would be picked for amd64 before /usr/lib64. Cross-compiling to arm64 on Debian would find the x86_64 multiarch directory first. Maybe the directory list could depend on GOARCH.

  3. For GOARCH=arm the triple becomes -gnu rather than -gnueabihf, which looks like it changes the float ABI. That doesn't match ld-linux-armhf.so.3 in the dynamic linker table.

  4. On 32-bit glibc, mmap takes a 32-bit off_t unless _FILE_OFFSET_BITS=64 is set, but the runtime passes an int64 offset (src/runtime/runtime_unix.go). musl always uses a 64-bit off_t, so this only shows up with glibc. Maybe limit -libc=glibc to amd64 and arm64 for now?

  5. Verify checks o.GOOS, so -libc=glibc combined with a non-Linux -target on a Linux host is accepted and then silently ignored. Might be worth returning an error in that case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants