Почему динамическая загрузка PIE больше не работает в glibc?

Примечание. Предполагаемый обман никоим образом НЕ отвечает на этот вопрос; в частности, локальный пример потока можно легко исправить, скомпилировав его с помощью -fPIC, точно так же, как уже упомянутый ниже пример bugzilla. А в остальном просто непрошенные мнения и безосновательные претензии.


В последних системах Linux, таких как Debian 10, RHEL 8 и т. д., вы можете создать файл ELF, который одновременно является независимым от позиции исполняемым файлом (PIE) и динамически загружаемой библиотекой/общим объектом.

Это очень полезно и имеет множество применений, таких как, например. создание программ-оболочек, которые предварительно загружают сами себя, а затем выполняют другую программу (см. Пример 2 ниже), или наличие виртуальной машины/языковой среды в виде единого объекта, который может быть либо встроен, либо работать как автономный интерпретатор. И все это без необходимости иметь дело с жесткими установочными каталогами, эксплойтами символических ссылок, $ORIGINs или другими подобными кошмарами безопасности и политики файловой системы.

Однако изменение 2c75b54 в glibc сломало его:

elf: Отказаться открывать объекты PIE [BZ #24323]

Другой исполняемый файл уже сопоставлен, поэтому динамический компоновщик не может правильно выполнить перемещение для второго исполняемого файла.

также утверждал в обсуждении, что привело к этому:

Также нет возможности правильно выполнить конструкторы ELF второго исполняемого файла.

Но это кажется фикцией. Как показано на Приложении 1 ниже, конструкторы, перемещения и локальные переменные потока работают нормально.

Перемещения копирования не работают, но есть ли причина также ломать программы, которые не используют перемещения копирования, как те, которые скомпилированы с помощью -fPIC? (В частности, тестовый пример из этого обсуждение можно легко исправить, просто скомпилировав его с помощью -fPIC).

Это изменение также было выбрано во FreeBSD 12.2, основная причина в том, что это делает glibc. слишком. Он по-прежнему работает в NetBSD 9.1 и OpenBSD 6.8 (хотя конструкторы не работают в OpenBSD).

Итак, каковы технические причины, по которым использование PIE таким образом не должно работать? Четкие сценарии, в которых это может сломаться (см. проблемы из рисунков 1 и 2), были бы замечательными.

Экспонат 1

Программа libexe должна работать так же, когда a) выполняется напрямую или b) dl-загружается как разделяемая библиотека и вызывается loader через функцию main.

Задача состоит в том, чтобы продемонстрировать возможности, которые libexe мог бы использовать, но которые препятствовали бы либо a), либо b) или заставляли бы его работать по-другому, что было бы трудно исправить.

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

Экспонат 2

Эта небольшая программа предварительно загружает себя, а затем запускает другой исполняемый файл. В отличие от типичного LD_PRELOAD=/some/path ./cmd, это также будет нормально работать через fexecve. /execveat(AT_EMPTY_PATH) в Linux, как при выполнении < файл href="https://man7.org/linux/man-pages/man2/memfd_create.2.html" rel="nofollow noreferrer">memfd_created. Он был протестирован для работы с Debian ›= 9, Centos/RHEL ›= 7 и т. д.

Задача состоит в том, чтобы продемонстрировать исполняемый файл, который потерпит неудачу при последовательном выполнении таким образом, но будет работать нормально, когда предварительно загруженный код находится в отдельной библиотеке, как в случае с 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

person Impudent Snob    schedule 03.11.2020    source источник
comment
Если исполняемый файл нельзя использовать в качестве общего объекта, возможно, вы могли бы создать общий объект, который можно использовать в качестве исполняемого файла.   -  person Zsigmond Lőrinczy    schedule 03.11.2020
comment
@ZsigmondLőrinczy Я полагаю, вы имеете в виду, что есть некоторые параметры компоновщика / скрипты компоновщика, которые сделают программу, скомпилированную с помощью -fPIC -shared, также пригодной для использования в качестве исполняемого файла. Кто они такие?   -  person Impudent Snob    schedule 04.11.2020