Linux как ограничить процесс по памяти

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

Читайте также:  Что потребляет оперативную память windows 10

Я использую приведенный ниже скрипт, который отлично работает. Он использует группы через 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. Начнём:

Читайте также:  Rtl8812au kali linux driver wikidevi

Теперь давайте ограничим память при помощи 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 к стеку с целью уменьшить место для кучи смысла не имеет, поскольку ядро разместит страницу в пустом пространстве.

Читайте также:  Windows help and support command line

Песочница

Ещё один способ ограничения памяти программы — 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.

Источник

Оцените статью