Всем привет! Потребовалось на старой 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 не сработает.
Комментарии (3)

Apoheliy
26.12.2025 16:29Последний раздел можно продолжить:
Использование ресурсов: Если вызовы функции делать в разных потоках, то возможно открытие нескольких дескрипторов, и при выходе удалится только один через функцию close_... . Также, если все вызовы делать не из "главного" потока, то возможно, что открытые дескрипторы вообще не будут освобождены. Однако, такое поведение не влияет на качество получаемых данных, а дескрипторы в любом случае будут закрыты по завершении процесса. Некоторое беспокойство следует проявлять только при очень большом количестве потоков с запросами случайных данных на архитектуре ARM.
Производительность: если открытие файла /dev/urandom всегда возвращает ошибку (нет прав или др.), то использование функций будет каждый раз тратить ресурсы процессора на попытку открытия файла. Это может сказаться на производительности, программисту рекомендуется запоминать факт возврата ошибки и в дальнейшем функцию не вызвать.
Особые случаи: при использовании getentropy возможна ошибка (возврат -1), когда код ошибки заполнен мусором (когда вычитано меньшее количество данных).
alan008
Хотел написать "мсье знает толк", но ведь и правда знает, и в данном случае это круто (независимо от странности решаемой проблемы!)
JBFW
Никогда не знаешь заранее что где пригодится...