clinuxld-preload

How can I read environment variables in the early stages?


Use LD_PRELOAD to load shared objects with the initfirst flag. Calling the getenv() function from a function with __attribute__((constructor)) returns NULL. I think this is probably because the function is being called before someone (libc?) initializes __environ.

// foo.c
#include <stdio.h>
#include <stdlib.h>

void setup(void) __attribute__((constructor));

void
setup(void)
{
    fprintf(stderr, "FOO=\"%s\"\n", getenv("FOO"));
}
$ cc -o foo.so -fPIC -shared foo.c -Wl,-z,initfirst
$ LD_PRELOAD=./foo.so FOO=123 ./hello
FOO="(null)"

Removing the initfirst flag produced the expected result.

Questions:

  1. Is there a way to read environment variables under these conditions? (It's fine if it only works on Linux, amd64, gcc, and glibc)
  2. Who initializes __environ? The loader, libc, crt0, or the kernel?

Environment: Ubuntu 24.04.1 LTS, amd64, gcc 13.3.0, glibc 2.39


Solution

  • Funtions marked with __attribute__((constructor)) take the same arguments as main, including the envp argument. Note that envp is a bad idea to use within main since it breaks if setenv is called, but here it is useful since environ has not yet been initialized.

    // foo.c
    #include <stdio.h>
    #include <stdlib.h>
    
    void setup(int argc, char **argv, char **envp) __attribute__((constructor));
    
    void
    setup(int argc, char **argv, char **envp)
    {
        fprintf(stderr, "argc: %d\n", argc);
        for (int i = 0; i < argc; ++i)
        {
            fprintf(stderr, "argv[%d]: %p\n", i, argv[i]);
        }
        for (char **p = envp; *p; ++p)
        {
            fprintf(stderr, "env: %s\n", *p);
        }
    }
    

    Since this question relies on dynamic linking, we can ignore all libc implementations that don't support that. Among those that do: