Note: The purported dupe does NOT answer this question in any way; in particular, the thread-local example can be easily fixed by compiling with -fPIC
, just like the bugzilla example already mentioned below. And the rest is just unasked-for opinions and baseless claims.
In recent Linux systems like Debian 10, RHEL 8, etc you can create an ELF file which is both a position independent executable (PIE) and a dynamically loaded library / shared object at the same time.
This is very useful, and has a lot of applications, like e.g. creating wrapper programs which preload themselves and then execute another program (see Exhibit 2 below), or having a virtual machine / language environment as a single object which could be either embedded or run as a standalone interpreter. All without having to deal with hardwired installation directories, symlink exploits, $ORIGIN
s or other such security and filesystem policy nightmares.
However, change 2c75b54 in glibc broke it:
elf: Refuse to dlopen PIE objects [BZ #24323]
Another executable has already been mapped, so the dynamic linker cannot perform relocations correctly for the second executable
also claiming in the discussion which led to it that:
There is also no way to correctly execute the ELF constructors of the second executable
But that seems bogus. As shown by Exhibit 1 below, constructors, relocations and thread-local variables seem to work fine.
Copy relocations do not work, but is there any reason to also break programs which do not use copy relocations, as those compiled with -fPIC
? (In particular, the testcase from that discussion could be easily fixed by just compiling it with -fPIC
).
This change was also picked in FreeBSD 12.2, the main reason given being that "glibc does it too". It still works in NetBSD 9.1 and OpenBSD 6.8 (though constructors do not work in OpenBSD).
So, what are the technical reasons why using PIEs in this manner should not work? Clear scenarios where this would break (see the challenges from Exhibit 1 and 2) would be great.
Exhibit 1
The libexe
program should work the same when a) executed directly or b) dl-loaded as a shared library and called via its main
function by loader
.
The challenge is to demonstrate features that libexe
could use that would preclude either a) or b) or cause it to work differently, in ways that cannot be easily fixed.
cat <<'EOT' > libexe.c
#include <stdio.h>
#include <errno.h>
#include <err.h>
__thread int var;
void set_errno(int e){ errno = e; }
__attribute__((weak))
int main(void){
set_errno(EPIPE); warn("%s var=%d", __FILE__, var);
}
__attribute__((constructor))
static void init(void){
var = 33;
fprintf(stderr, "%s's constructor\n", __FILE__);
}
EOT
cat <<'EOT' > loader.c
#include <dlfcn.h>
#include <stdio.h>
#include <err.h>
#include <errno.h>
int main(void){
void *dl; char *lib = "./libexe";
if(!(dl = dlopen(lib, RTLD_LAZY)))
errx(1, "dlopen: %s", dlerror());
printf("var=%d in %s\n", *(int*)dlsym(dl, "var"), __FILE__);
((void(*)(int))dlsym(dl, "set_errno"))(EBADF); warn("%s", __FILE__);
return ((int(*)(void))dlsym(dl, "main"))();
}
__attribute__((constructor))
static void init(void){
fprintf(stderr, "%s's constructor\n", __FILE__);
}
EOT
cc -pie -fPIC -Wl,-E libexe.c -o libexe
cc loader.c -o loader -ldl
############
$ ./loader
loader.c's constructor
libexe.c's constructor
var=33 in loader.c
loader: loader.c: Bad file descriptor
loader: libexe.c var=33: Broken pipe
Exhibit 2
This little program preloads itself and then runs another executable. Unlike the typical LD_PRELOAD=/some/path ./cmd
, this would also run fine via fexecve
/execveat(AT_EMPTY_PATH)
on Linux, as when exec'ing a memfd_create
d file. It was tested to work on Debian >= 9, Centos/RHEL >= 7, etc.
The challenge is to demonstrate an executable which would fail when chain-exec'ed in this way, but will work fine when the preloaded code is in a separate library, as with stdbuf
/libstdbuf.so
.
cat <<'EOT' > read-eio.c
#define _GNU_SOURCE
#include <unistd.h>
#include <err.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
int main(int ac, char **av){
int fd; char buf[32];
if(ac < 2 || !av[1])
errx(1, "usage: %s cmd args..", av[0]);
if((fd = open("/proc/self/exe", O_PATH)) == -1)
err(1, "open /proc/self/exe");
snprintf(buf, sizeof buf, "/dev/fd/%d", fd);
if(setenv("LD_PRELOAD", buf, 1))
err(1, "setenv");
execvp(av[1], av + 1);
err(1, "execvp %s", av[1]);
}
ssize_t read(int fd, void *b, size_t z){
errno = EIO; return -1;
}
EOT
cc -fPIC -pie read-eio.c -o read-eio
###########
$ ./read-eio cat
cat: -: Input/output error