There is a strange behavior around GO executable built in Alpine images where standard LD_PRELOAD feature is not working correctly.
It looks like constructor functions are not called by the dynamic loader!
I have an example go application (getgoogle.go
):
package main
import (
"fmt"
"net/http"
)
func main() {
resp, err := http.Get("http://google.com/")
if err == nil {
fmt.Println(resp.StatusCode)
}
}
And the example shared object code (libldptest.c
)
#include <stdio.h>
static void __attribute__((constructor)) StaticConstructor(int argc, char **argv, char **env)
{
printf(">>> LD_PRELOADED!n");
}
I am creating a debian based docker image with this Dockerfile (gotest
image):
FROM golang
COPY libldptest.c hello-world.go /
RUN gcc -shared -o /libldptest.so /libldptest.c
RUN go build -gcflags='-N -l' -o /getgoogle /getgoogle.go
ENV LD_PRELOAD=/libldptest.so
Then running the following command:
$docker run -it gotest /getgoogle
>>> LD_PRELOADED!
200
This means the constructor works here.
But when doing the same with an alpine based docker image
FROM golang:1.12-alpine
RUN apk add gcc libc-dev
COPY libldptest.c hello-world.go /
RUN gcc -shared -o /libldptest.so /libldptest.c
RUN go build -gcflags='-N -l' -o /getgoogle /getgoogle.go
ENV LD_PRELOAD=/libldptest.so
And running the same command as above
$docker run -it gotest /getgoogle
200
$docker run -it gotest ls
>>> LD_PRELOADED!
bin src
Meaning the static constructor was not called when running the go application! (but is was called when running ls
)
Note that I have checked that the dynamic loader adds the library to the process space.
I’d be grateful to understand why it is not working.
2
Answers
There is a principal problem with static constructors in the Go/Alpine environment as one can see from the comments. In short, from an ABI perspective, the requirement to call static constructors is assigned to the executable and not the loader. Go executable is no based on C runtime and it only calls the static constructors of the dependency shared objects and not
LD_PRELOAD
-ed shared objects. In case of glibc the constructors of aLD_PRELOAD
-ed shared objects called by implementation and not by design by the loader. On musl-libc they are not.I have made a "hack"-ish workaround to make existing Go apps work with a
LD_PRELOAD
-ed shared object. I am using the fact that the library isLD_PRELOAD
-ed in this environment correctly by musl-libc and that Go is callingpthread_create
in very early stage of the initialization.I am overriding/hooking the
pthread_create
symbol in theLD_PRELOAD
-ed shared object and uses it to call constructors.Caveats: this works with current Go runtime, but the assumptions on this solution is built are far from being future proof. Next Go releases can easily break it.
Stop ignoring the first comment. If you insist on using Go’s internal linker that does not link in a way that’s compatible with libc use then you can’t use any C code, including
LD_PRELOAD
ed C code or even features of the dynamic linker itself. As Florian (from glibc) said in the linked issue, it is not valid with glibc either and “working” only by chance there.Even if you somehow figure out “mechanically” why your ctor isn’t being called, you’re still running C code in a corrupted process state and anything could go wrong. Even if you analyze everything and it seems fine, this can change entirely with next dynamic linker/libc update.
If you want to do this, use the external linker option in Go.