skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 a LD_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 is LD_PRELOAD-ed in this environment correctly by musl-libc and that Go is calling pthread_create in very early stage of the initialization.

    I am overriding/hooking the pthread_create symbol in the LD_PRELOAD-ed shared object and uses it to call constructors.

    #include <pthread.h>
    #include <dlfcn.h>
    
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
    {
        int (*pthread_create_original)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) = dlsym(RTLD_NEXT,"pthread_create");
        static int already_called = 0;
    
        if (!already_called)
        {
            already_called = 1;
            // call here your constructors
        }
    
        return pthread_create_original(thread,attr,start_routine,arg);
    }
    

    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.


  2. 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_PRELOADed 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search