System function overloading in nested shared library

Viewed 69

Simple scenario

  1. Application uses write function from libc and links to shared library.
  2. Shared library defines write function.
  3. Original write function from libc will be overloaded by its version from the shared library.

Nested scenario

  1. Application uses write function from libc.
  2. Shared library 1 does not defines its own write but depends on shared library 2.
  3. Shared library 2 defines write function.
  4. No. write function will NOT be replaced with a version from the second shared library.

I do want to understand Why so ? How to make it work with nested shared library dependencies.

Here is exact code examples:

main.c

#include <unistd.h>

int main() {

    write(1,"Hello\n",6);
    return 0;
}

shared-lib-1.c

#include <unistd.h>

__attribute__((constructor))
void shared_lib_1_constructor(void) {
    char str[] = "shared-lib-1-constructor\n";
    write(1, str, sizeof(str)); 
}

shared-lib-2.c

#include <stdlib.h>
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count) {
    exit(1);
    return 0;
}

__attribute__((constructor))
void shared_lib_2_constructor(void) {
    char str[] = "shared-lib-2-constructor\n";
    write(1, str, sizeof(str)); 
}

build.sh

#!/usr/bin/env sh

set -v
set -e

gcc -g -fPIC -shared shared-lib-2.c -o libshared-lib-2.so
gcc -g -fPIC -shared -Wl,-rpath . -L`pwd` -lshared-lib-2 shared-lib-1.c -o libshared-lib-1.so
gcc -g -L`pwd` -Wl,-rpath . -lshared-lib-1 main.c

Execution of the a.out gives:

[smirnov@localhost tmp]$ ./a.out 
shared-lib-2-constructor
shared-lib-1-constructor
Hello

write was not overwritten by shared-lib-2. It was completely ignored even from shared-lib-2 code.

How does it work with nested libraries ? Moving write definition to shared-lib-1 does work, it overloads glibc version and exits from application.

Running application with LD_DEBUG=all shows order of resolving:

   36441:     Initial object scopes
   36441:     object=./a.out [0]
   36441:      scope 0: ./a.out ./libshared-lib-1.so /lib64/libc.so.6 ./libshared-lib-2.so /lib64/ld-linux-x86-64.so.2
...
   36441:     calling init: ./libshared-lib-2.so
   36441:     
   36441:     symbol=write;  lookup in file=./a.out [0]
   36441:     symbol=write;  lookup in file=./libshared-lib-1.so [0]
   36441:     symbol=write;  lookup in file=/lib64/libc.so.6 [0]
   36441:     binding file ./libshared-lib-2.so [0] to /lib64/libc.so.6 [0]: normal symbol `write'

Why libc placed in between shared-lib-1 and shared-lib-2 ? It seems that ld resolves in the following order:

  • all dependencies of a.out, but not recursivelly, only first level
  • all subdependencies, but not recursivelly
  • so on...

The only solution I've found is to use

LD_DYNAMIC_WEAK=1 ./a.out

Is there a way to fix behavior in advance ?

1 Answers

write function will NOT be replaced with a version from the second shared library.

This is not what I expected, and it took me a while to figure out why it's happening.

The issue is that libc.so.6 doesn't define write function (if it did, everything would have worked the same whether the dependency is direct or not).

Instead, libc.so.6 defines a versioned write@@GLIBC_2.2.5 symbol. It is that symbol versioning (a GNU extension) which interferes with your desired outcome.

Compare:

gcc main.c ./libshared-lib-1.so && nm -D a.out | grep ' write'
                 U write@GLIBC_2.2.5
gcc main.c ./libshared-lib-2.so && nm -D a.out | grep ' write'
                 U write

When performing symbol resolution, the loader looks up symbol version in the search scope (and a.out is at the front). With libshared-lib-1.so, the loader finds that it should be looking for write version GLIBC_2.2.5 (which can only be found in libc.so.6), and ignores the un-versioned definition in libshared-lib-2.so.

Is there a way to fix behavior in advance ?

If you can control the application link line, it's best to link a.out with libshared-lib-2.so:

gcc main.c ./libshared-lib-1.so ./libshared-lib-2.so
./a.out
# no output as expected.

If you can't control the application link line, you should be able to provide a versioned write@GLIBC_2.2.5 using the linker --version-script flag and appropriate version script. But my initial attempt at doing that failed, and I am not sure this approach can be made to work.

Related