Всем привет! Потребовалось на старой Synology (ядро linux 3.10, нет возможности обновить) запустить несколько docker-контейнеров, требующих getrandom и/или getentropy. Но старые ядра не имеют этих системных вызовов. Напр��мер, последние версии контейнеров веб-сервера apache выдают такую ошибку: [:crit] (38)Function not implemented: AH00141: Could not initialize random number generator.

Но в старых Linux есть /dev/random и /dev/urandom. Начал думать как решить эту проблему, вспомнил про LD_PRELOAD — это переменная окружения в Linux, которая указывает, какая разделяемая библиотека должна быть загружена до любых других библиотек. Она используется для переопределения функций в библиотеках по умолчанию. Обычно используется для отладки, тестирования, разработки, etc.

При выполнении программы dynamic linker (ld.so) ищет необходимые для программы разделяемые библиотеки. Если LD_PRELOAD установлен, указанная библиотека загружается первой, даже раньше стандартных библиотек, таких как libc.

Например, если использовать библиотеку library.so,export LD_PRELOAD=/path/to/your/library.so, то функции, определенные в файле library.so, будут иметь приоритет над функциями в библиотеках по умолчанию.

В контексте решения проблемы запуска современных docker контейнеров на системах со старым ядром, создадим файл randentoldkernel.c с таким содержимым:

#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/syscall.h>

static int urandom_fd = -1;

// Функция для инициализации дескриптора
static int get_fd() {
    if (urandom_fd == -1) {
        // Открываем с флагом O_CLOEXEC, чтобы дескриптор не наследовался 
        // дочерними процессами через exec()
        int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
        if (fd != -1) {
            urandom_fd = fd;
        }
    }
    return urandom_fd;
}

// Автоматическое закрытие при завершении работы программы
__attribute__((destructor))
static void close_urandom_fd() {
    if (urandom_fd != -1) {
        close(urandom_fd);
        urandom_fd = -1;
    }
}

// Реализация getrandom
ssize_t getrandom(void *buf, size_t buflen, unsigned int flags) {
    int fd = get_fd();
    if (fd == -1) {
        errno = ENOSYS;
        return -1;
    }
    return read(fd, buf, buflen);
}

// Реализация getentropy
int getentropy(void *buf, size_t buflen) {
    if (buflen > 256) {
        errno = EIO;
        return -1;
    }
    
    int fd = get_fd();
    if (fd == -1) {
        errno = ENOSYS;
        return -1;
    }

    ssize_t res = read(fd, buf, buflen);
    if (res != (ssize_t)buflen) {
        return -1;
    }
    return 0;
}

Далее скомпилируем файл randentoldkernel.c. Для компиляции будем использовать временные контейнеры docker, так как для того чтобы файл корректно работал в разных дистрибутивах (например, Alpine использует musl, а Ubuntu/Debian — glibc), лучше всего скомпилировать две версии библиотеки в соответствующих Docker-контейнерах. Это гарантирует правильную линковку.

Сборка для glibc (Ubuntu, Debian, CentOS, Fedora)

docker run --rm -v "$PWD":/src -w /src debian:stable-slim /bin/sh -c "\
    apt update && apt install -y gcc libc6-dev && \
    gcc -shared -fPIC randentoldkernel.c -o randentoldkernel-glibc.so"

Сборка для musl (Alpine Linux)

docker run --rm -v "$PWD":/src -w /src alpine:latest /bin/sh -c "\
    apk add --no-cache gcc musl-dev && \
    gcc -shared -fPIC randentoldkernel.c -o randentoldkernel-musl.so"

Также используя кросс-компиляцию можно собрать библиотеки под архитектуры, отличные от x86-64, но это отдельная тема.

Проверим полученные 2 файла randentoldkernel-glibc.so и randentoldkernel-musl.so с помощью ldd (ниже вывод с последнего Debian-a):

еlir@debian:~/tests$ ldd randentoldkernel-glibc.so
linux-vdso.so.1 (0x00007f5bbecb4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5bbeaad000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5bbecb6000)

еlir@debian:~/tests$ ldd randentoldkernel-musl.so
/lib64/ld-linux-x86-64.so.2 (0x00007f9811318000)
linux-vdso.so.1 (0x00007f9811316000)
libc.musl-x86_64.so.1 => not found

Как видим, библиотека randentoldkernel-musl.so не запустится на Debian из-за отсутствия musl, но будет работать внутри docker-контейнера, основанного на Alpine.

Далее скопируем скомпилированную библиотеку в необходимый образ Docker и укажем LD_PRELOAD внутри образа. Создадим Dockerfile и добавим в него следующее:

Для образов основанных на Debian

FROM httpd:latest
COPY randentoldkernel-glibc.so /usr/local/lib/randentoldkernel-glibc.so
ENV LD_PRELOAD="/usr/local/lib/randentoldkernel-glibc.so"

Для образов основанных на Alpine

FROM alpine:latest
COPY randentoldkernel-musl.so /usr/local/lib/randentoldkernel-musl.so
ENV LD_PRELOAD="/usr/local/lib/randentoldkernel-musl.so"

Затем соберём образ: docker build -t imagename:1.0 .

Образ можно сразу же экспортировать в tar: docker save -o imagename.tar imagename:1.0

Теперь наши образы будут работать на системах со старым ядром.

Безопасность: Разница использования /dev/urandom вместо getentropy() / getrandom() в том, что системный вызов getrandom() по умолчанию блокирует выполнение, если пул энтропии ядра еще не инициализирован (например, сразу после загрузки системы), а /dev/urandom — нет. getentropy() обычно является оберткой над getrandom(). Если сервер генерирует SSL-ключи в первые секунды после загрузки старого ядра, ключи могут быть менее стойкими. Рекомендуется отложенный старт для таких приложений.

Также если злоумышленник получит доступ к записи в файл библиотеки randentoldkernel.so или сможет изменить переменную LD_PRELOAD, он сможет подменить любую другую функцию. Но в Docker-контейнере этот риск минимален, так как файловая система образа обычно неизменяема для внешнего мира, а переменные окружения задаются при старте. Этот способ не открывает прямой дыры в безопасности. Это гораздо безопаснее, чем запускать устаревшие версии Apache с известными CVE.

Основной нюанс: если приложение или его модули вызывают getrandom через прямой ассемблерный вызов syscall(), а не через стандартную библиотеку libc, то LD_PRELOAD не сработает.