Ограничить использование памяти для одного процесса Linux
Я запускаю pdftoppm , чтобы преобразовать предоставленный пользователем PDF в изображение 300DPI. Это отлично работает, за исключением случаев, когда пользователь предоставляет PDF с очень большим размером страницы. pdftoppm выделяет достаточно памяти для хранения изображения 300DPI такого размера в памяти, что для 100-дюймовой квадратной страницы составляет 100 * 300 * 100 * 300 * 4 байта на пиксель = 3,5 ГБ. Злоумышленник может просто дать мне глупый PDF-файл и вызвать всевозможные проблемы.
Итак, что бы я хотел сделать, это установить какой-то жесткий лимит на использование памяти для дочернего процесса, который я собираюсь запустить — просто запустите процесс, если он попытается выделить больше, чем, скажем, 500 МБ Память. Возможно ли это?
Я не думаю, что для этого можно использовать ulimit, но есть ли однопроцессный эквивалент?
5 ответов
Есть проблемы с ulimit. Вот полезное чтение по теме: Ограничение времени и памяти для программы в Linux , что приводит к тайм-ауту , что позволяет вы привязываете процесс (и его вилки) по времени или потреблению памяти.
Для тайм-аута требуется установить Perl 5+ и файловую систему /proc . После этого вы копируете инструмент, например. /usr/local/bin :
После этого вы можете «привязать» свой процесс к потреблению памяти, как в своем вопросе, например:
В качестве альтернативы вы можете использовать -t и -x , чтобы соответственно ограничить процесс по времени или ограничениям на cpu.
Как работает этот инструмент, проверяя несколько раз в секунду, если порожденный процесс не превысил установленные границы. Это означает, что на самом деле есть небольшое окно, в котором процесс мог бы потенциально переподписываться перед уведомлениями о тайм-ауте и убивает процесс.
Более правильный подход, скорее всего, будет связан с группами, но это гораздо более важно для настройки, даже если вы будете использовать Docker или runC, которые, среди прочего, предлагают более удобную абстракцию вокруг групп.
Другой способ ограничить это — использовать контрольные группы Linux. Это особенно полезно, если вы хотите ограничить выделение (или группу процессов) процесса физической памятью из виртуальной памяти. Например:
создаст контрольную группу с именем myGroup , закроет набор процессов, выполняемых под myGroup, до 500 МБ физической памяти и до 5000 Мбайт свопа. Чтобы запустить процесс в группе управления:
Обратите внимание, что в современном дистрибутиве Ubuntu этот пример требует установки пакета cgroup-bin и редактирования /etc/default/grub для изменения GRUB_CMDLINE_LINUX_DEFAULT to:
, а затем запустите sudo update-grub и перезагрузитесь для загрузки с новыми параметрами загрузки ядра.
Если ваш процесс не порождает больше детей, которые потребляют большую часть памяти, вы можете использовать setrlimit . Более общий пользовательский интерфейс для этого заключается в использовании команды ulimit оболочки:
Это ограничит только «виртуальную» память вашего процесса, принимая во внимание «и ограничивая» память, в которой процесс, вызываемый совместно с другими процессами, и память, отображаемая, но не зарезервированная (например, большая куча Java) , Тем не менее, виртуальная память является ближайшим приближением для процессов, которые растут очень большими, делая указанные ошибки несущественными.
Если ваша программа порождает детей, а именно они выделяют память, она становится более сложной, и вы должны писать вспомогательные скрипты для запуска процессов под вашим контролем. Я написал в своем блоге почему и как .
В дополнение к инструментам из daemontools , предложенным Марком Джонсоном, вы также можете рассмотреть chpst , который находится в runit . Сам Runit находится в busybox , поэтому вы, возможно, уже установили его.
-m байт ограничить память. Ограничьте сегмент данных, сегмент стека, заблокированные физические страницы и общее количество всего сегмента за процесс до байтов байтов каждая.
Я использую приведенный ниже скрипт, который отлично работает. Он использует группы через cgmanager . Обновление: теперь он использует команды из cgroup-tools . Назовите этот скрипт limitmem и поместите его в свой $ PATH, и вы можете использовать его как limitmem . Это ограничит использование памяти и свопа. Чтобы ограничить только память, удалите строку с помощью limitmem 100M bash .
Источник
Ограничение памяти, доступной программе
Решил я как-то заняться задачкой сортировки миллиона целых чисел при имеющейся памяти в 1 Мб. Но перед этим мне пришлось подумать над тем, как можно ограничить объём доступной памяти для программы. И вот, что я придумал.
Виртуальная память процесса
Перед тем, как окунуться в разные методы ограничения памяти, необходимо знать, как устроена виртуальная память процесса. Лучшая статья на эту тему — «Анатомия программы в памяти».
Прочитав статью, я могу предложить две возможности для ограничения памяти: уменьшить виртуальное адресное пространство или объём кучи.
Первое: уменьшение объёма адресного пространства. Это довольно просто, но не совсем корректно. Мы не можем уменьшить всё пространство до 1 Мб — не хватит места для ядра и библиотек.
Второе: уменьшение объёма кучи. Это не так-то просто сделать, и обычно так никто не делает, поскольку это доступно только через возню с компоновщиком. Но для нашей задачи это был бы более корректный вариант.
Также я рассмотрю другие методы, такие, как отслеживание использования памяти через перехват вызовов библиотек и системы, и изменение окружения программы через эмуляцию и введение «песочницы».
Для тестирования будем использовать небольшую программу по имени big_alloc, размещающую, и затем освобождающую 100 MiB.
Все исходники есть на github.
ulimit
То, о чём сразу вспоминает старый unix-хакер, когда ему нужно ограничить память. Это утилита из bash, которая позволяет ограничивать ресурсы программы. На деле это интерфейс к setrlimit.
Мы можем установить ограничение на объём памяти для программы.
Мы задали ограничение в 1024 кб — 1 MiB. Но если мы попытаемся запустить программу, она отработает без ошибок. Несмотря на лимит в 1024 кб, в top видно, что программа занимает аж 4872 кб.
Причина в том, что Linux не устанавливает жёстких ограничений, и в man об этом написано:
Есть также опция ulimit -d, которая должна работать, но всё равно не работает из-за mmap (см. раздел про компоновщик).
Для манипуляции программным окружением QEMU прекрасно подходит. У неё есть опция –R для ограничения виртуального адресного пространства. Но до слишком малых значений его ограничивать нельзя – не поместятся libc и kernel.
Тут -R 1048576 оставляет 1 MiB на виртуальное адресное пространство.
Для этого надо отвести что-то порядка 20 MB. Вот:
Останавливается после 100 итераций (10 MB).
В общем, QEMU пока лидирует среди методов для ограничения, надо только поиграться с величиной –R.
Контейнер
Ещё вариант – запустить программу в контейнере и ограничить ресурсы. Для этого можно:
- использовать какой-нибудь docker
- использовать инструменты usermode из пакета lxc
- написать свой скрипт с libvirt.
- что-то ещё…
Но ресурсы будут ограничены при помощи подсистемы Linux под названием cgroups. Можно играться с ними напрямую, но я рекомендую через lxc. Я бы хотел использовать docker, но он работает только на 64-битных машинах.
LXC — это LinuX Containers. Это набор инструментов и библиотек из userspace для управления функциями ядра и создания контейнеров – изолированных безопасных окружений для приложений, или для всей системы.
Функции ядра следующие:
- Control groups (cgroups)
- Kernel namespaces
- chroot
- Kernel capabilities
- SELinux, AppArmor
- Seccomp policies
Документацию можно найти на офсайте или в блоге автора.
Для запуска приложения в контейнере необходимо предоставить lxc-execute конфиг, где указать все настройки контейнера. Начать можно с примеров в /usr/share/doc/lxc/examples. Man рекомендует начать с lxc-macvlan.conf. Начнём:
Теперь давайте ограничим память при помощи cgroup. LXC позволяет настроить подсистему памяти для cgroup контейнера, задавая ограничения памяти. Параметры можно найти в документации по RedHat. Я нашёл 2:
- memory.limit_in_bytes — задаёт максимальное количество пользовательской памяти, включая файловый кэш
- memory.memsw.limit_in_bytes — задаёт максимальное количество в сумме памяти и свопа
Что я добавил в lxc-my.conf:
Тишина — видимо, памяти слишком мало. Попробуем запустить из шелла
bash не запустился. Попробуем /bin/sh:
И в dmesg можно отследить славную смерть процесса:
Хотя мы не получили сообщение об ошибке от big_alloc насчёт malloc failure и количества доступной памяти, мне кажется, мы удачно ограничили память при помощи контейнеров. Пока остановимся на этом
Компоновщик
Попробуем изменить бинарный образ, ограничив доступное куче место. Компоновка – последний этап построения программы. Для этого используется компоновщик и его скрипт. Скрипт – описание разделов программы в памяти вместе со всякими атрибутами и прочим.
Пример компоновочного скрипта:
Точка означает текущее положение. Например, раздел .text начинается с адреса 0×10000, а затем, начиная с 0×8000000 у нас есть два следующих раздела: .data и .bss. Точка входа — main.
Всё круто, но в реальных программах работать не будет. Функция main, которую вы пишете в программах на С, реально не является первой вызываемой. Сперва совершается очень много инициализаций и подчисток. Этот код содержится в библиотеке времени исполнения С (crt) и распределено по библиотекам crt#.o в /usr/lib.
Подробности можно увидеть, запустив gcc –v. Сначала она вызывает ccl, создаёт ассемблерный код, транслирует в объектный файл через as и в конце собирает всё вместе с ELF при помощи collect2. collect2 — обёртка ld. Она принимает объектный файл и 5 дополнительных библиотек, чтобы создать конечный бинарный образ:
Всё это очень сложно, поэтому вместо написания собственного скрипта я отредактирую скрипт компоновщика по умолчанию. Получим его, передав -Wl,-verbose в gcc:
Теперь подумаем, как его изменить. Посмотрим, как бинарник строится по умолчанию. Скомпилируем и поищем адрес раздела .data. Вот выдача objdump -h big_alloc
Разделы .text, .data и .bss расположены около 128 MiB.
Посмотрим, где стек, при помощи gdb:
esp указывает на 0xbffff0a0, что около 3 GiB. Значит, у нас есть куча в
В реальном мире верхний адрес стека случайный, его можно увидеть, например, в выдаче:
Как мы знаем, куча растёт от конца .data по направлению к стеку. Что, если мы подвинем раздел .data как можно выше?
Давайте разместим сегмент данных в 2 MiB перед стеком. Берём верх стека, вычитаем 2 MiB:
0xbffff0a0 — 0x200000 = 0xbfdff0a0
Смещаем все разделы, начинающиеся с .data на этот адрес:
Опции -Wl и -T hack.lst говорят компоновщику, чтобы он использовал hack.lst в качестве сценария работы.
Посмотрим на заголовок:
И всё равно данные размещаются в памяти. Как? Когда я попытался посмотреть значения указателей, возвращаемых malloc, я увидел, что размещение начинается где-то после окончания раздела.data по адресам вроде 0xbf8b7000, постепенно продолжается с увеличением указателей, а затем опять возвращается к нижним адресам вроде 0xb5e76000. Выглядит так, будто куча растёт вниз.
Если подумать, ничего странного в этом нет. Я проверил исходники glibc и выяснил, что когда brk не справляется, то используется mmap. Значит, glibc просит ядро разместить страницы, ядро видит, что у процесса куча дыр в виртуальной памяти, и размещает в одном из пустых мест страницу, после чего glibc возвращает указатель с неё.
Запуск big_alloc под strace подтвердил теорию. Посмотрите на нормальный бинарник:
А теперь на модифицированный:
Сдвиг раздела .data к стеку с целью уменьшить место для кучи смысла не имеет, поскольку ядро разместит страницу в пустом пространстве.
Песочница
Ещё один способ ограничения памяти программы — sandboxing. Отличие от эмуляции в том, что мы ничего не эмулируем, а просто отслеживаем и контролируем некоторые вещи в поведении программы. Обычно используется в исследованиях в области безопасности, когда вы изолируете зловреда и анализируете его так, чтобы он не нанёс вреда вашей системе.
Трюк с LD_PRELOAD
LD_PRELOAD — специальная переменная окружения, заставляющая динамический компоновщик использовать в приоритете предзагруженные библиотеки, в т.ч. libc. Этот трюк, кстати, также используют и некоторые зловреды.
Я написал простую песочницу, перехватывающую вызовы malloc/free, работающую с памятью и возвращающую ENOMEM по исчерпанию лимита.
Для этого я сделал библиотеку общего пользования (shared library) c моими реализациями вокруг malloc/free, увеличивающими счётчик на объём malloc, и уменьшающими, когда вызывается free. Она предзагружается через LD_PRELOAD.
Моя реализация malloc:
libc_malloc — указатель на оригинальный malloc из libc. no_hook локальный флаг в потоке. Используется для того, чтобы можно было использовать malloc в хуках и избежать рекурсивных вызовов.
malloc используется неявно в функции account библиотекой uthash. Зачем использовать таблицу хешей? Потому, что при вызове free вы передаёте в неё только указатель, а внутри free неизвестно, сколько памяти было выделено. Поэтому у вас есть таблица с указателями-ключами и объёмом размещённой памяти в виде значений. Вот что я делаю в malloc:
mem_allocated это та статическая переменная, которую сравнивают с ограничением в malloc.
Теперь при вызове free происходит следующее:
Да, просто уменьшаем mem_allocated.
И что самое крутое — это работает.
Полный код библиотеки на github
Получается, что LD_PRELOAD – отличный способ ограничить память
ptrace
ptrace — ещё одна возможность для построения песочницы. Это системный вызов, позволяющий управлять выполнением другого процесса. Встроен в различные POSIX ОС.
Это основа таких трассировщиков, как strace, ltrace, и почти всех программ для создания песочниц — systrace, sydbox, mbox и дебаггеров, включая gdb.
Я сделал свой инструмент при помощи ptrace. Он отслеживает вызовы brk и меряет расстояние между изначальным значением break и новым, которое задаётся следующим вызовом brk.
Программа форкается и запускает 2 процесса. Родительский – трассировщик, а дочерний – трассируемый. В дочернем процессе я вызываю ptrace(PTRACE_TRACEME) и затем execv. В родительском использую ptrace(PTRACE_SYSCALL) чтобы остановиться на syscall и отфильтровать вызовы brk из дочернего, а затем ещё один ptrace(PTRACE_SYSCALL) для получения значения, возвращаемого brk.
Когда brk выходит за заданное значение, я выставляю -ENOMEM в качестве возвращаемого значения brk. Это задаётся в регистре eax, поэтому я просто перезаписываю его с ptrace(PTRACE_SETREGS). Вот самая вкусная часть:
Также я перехватываю вызовы mmap/mmap2, так как у libc хватает мозгов вызывать их при проблемах с brk. Так что когда заданное значение превышено и я вижу вызов mmap, я обламываю его с ENOMEM.
Но мне это не нравится. Это завязано на ABI, т.е. тут приходится использовать rax вместо eax на 64-битной машине, поэтому надо либо делать отдельную версию, или использовать #ifdef, или принудительно использовать опцию -m32 option. И скорее всего не будет работать на других POSIX-подобных системах, у которых может быть другой ABI.
Иные способы
Что ещё можно попробовать (эти варианты были отвергнуты по разным причинам):
- хуки malloc. В man написано, что уже не поддерживаются
- Seccomp и prctl при помощи PR_SET_MM_START_BRK. Может сработать – но, как сказано в документации, это не песочница, а способ минимизации доступной поверхности ядра. То есть, это будет ещё более криво, чем использовать ручной ptrace
- libvirt-sandbox. Всего лишь обёртка для lxc и qemu.
- SELinux sandbox. Не работает, ибо использует cgroup.
Источник