Трендовые github проекты в нашем телеграм канале. Подпишись → setsockopt вернул 0, пакет всё равно пришёл: как проверять ограничения для инструментов агента
ИИ-агент обычно работает в песочнице — изолированной Linux-среде на контейнерном рантайме или виртуалке, где ему разрешено менять файлы и запускать команды. Отдельным инструментам внутри сессии нужно меньше прав. Анализатору кода достаточно читать исходники; если ему доступны все права агентной сессии, ошибка в нём испортит рабочий каталог, даже когда внешняя изоляция исправно защищает хост.
Сужать права должна доверенная обвязка, которая запускает инструмент, и делать это до передачи ему управления. Инструкция модели «не меняй файлы» права процесса не уменьшает. Перед запуском обвязке нужно ответить на три вопроса:
- Есть ли выбранный механизм в этой среде.
- Хватает ли прав, чтобы его настроить.
- Действует ли запрет после настройки.
На третий вопрос отвечает только попытка выполнить запрещённую операцию. Эксперименты с Linux и gVisor показывают, что все три вопроса могут дать отрицательный ответ, причём последний — молча.
Стенд
- Хост: Ubuntu 22.04, ядро
6.8.0-138-generic, x86-64. - gVisor: релизы
20260817.0и20260831.0. - Пробы запускались через
runsc do, ключевые проверки BPF и сокетного фильтра повторялись черезrunsc runс OCI-бандлом. - Привилегированный профиль:
CapEff=000001ffffffdfff(безCAP_NET_RAW). Непривилегированный:CapEff=0. - В OCI-проверках
NoNewPrivs=0иSeccomp=0, то есть это не полный профиль рабочего пода. Для своей среды проверку надо повторять с её настройками.
Сценарий 1: механизма нет
Landlock — штатный способ в Linux, которым процесс может сам ограничить себе операции с файловой системой. Непривилегированный процесс сначала включает no_new_privs, чтобы exec другой программы не поднял ему права. Набор доступных ограничений зависит от версии ABI Landlock.
Под gVisor системные вызовы обрабатывает собственное ядро Sentry, поэтому Landlock на хосте ничего не говорит о Landlock в песочнице. В обоих релизах gVisor все три вызова Landlock возвращали ENOSYS — включая запрос версии ABI, которому не нужны ни правила, ни пути. На хосте тот же запрос возвращал ABI 4. В таблицах системных вызовов обоих релизов обработчиков Landlock нет, неизвестные номера получают ENOSYS.
Для обвязки это ранний и честный отказ. Если она проглотит ошибку и продолжит запуск, инструмент останется без запланированного ограничения.
Попутно всплыла ловушка с тестами. Штатные selftests Landlock под gVisor дали 194 отказа, но 179 из них случились ещё на подготовке: упёрлись в PR_SET_SECUREBITS, пространства имён и каталог, оставленный предыдущим упавшим тестом. До проверяемой операции они не дошли. На хосте был один отказ: тест из upstream v6.8 считал флаг LANDLOCK_CREATE_RULESET_ERRATA недопустимым, а дистрибутивное ядро его принимало — исправление бэкпортировали, сохранив ABI 4. Два практических правила отсюда:
- наличие функции проверяйте через её интерфейс, по
unameверсию ядра угадывать бесполезно; - при упавшем тесте сначала выясните, дошёл ли он до проверяемой операции.
Сценарий 2: настройка проходит, эффекта нет
BPF-программа типа CGROUP_DEVICE управляет доступом процессов к устройствам, такие правила может ставить привилегированная обвязка. В gVisor включили cgroup v2 и загрузили программу, запрещающую доступ ко всем устройствам. Результат по шагам:
| Шаг | Linux на хосте | gVisor с нужными capability |
|---|---|---|
Загрузка CGROUP_DEVICE | дескриптор получен | дескриптор получен |
| Подключение к cgroup | успех | успех |
| Запрос подключённых программ | программа найдена | программа найдена |
| Открытие устройства под запретом | EPERM | успех |
На хосте контрольный дочерний процесс в отдельной cgroup открывал /dev/null до подключения программы, получал EPERM после и снова открывал после отключения. В gVisor /dev/null и /dev/zero открывались, в том числе в новом процессе, вошедшем в cgroup уже после подключения фильтра. Документация gVisor об этом предупреждает: загрузка, подключение и опрос поддерживаются, сами программы эффекта не имеют. Проверка всех трёх «административных» шагов действие запрета не подтверждает.
Для загрузки в этих релизах нужен CAP_SYS_ADMIN либо пара CAP_BPF + CAP_NET_ADMIN. При CapEff=0 загрузка возвращала EPERM. Права того, кто ставит ограничение, и того, кто под ним работает, могут различаться, так что привилегированная обвязка всё равно может поставить такой фильтр за непривилегированный процесс — и получить ту же пустышку. Для eBPF-мониторинга нужны другие операции (создание карт, программы типа TRACEPOINT), и в тех же замерах они отвергались.
Сценарий 3: то же самое без привилегий
Классический BPF-фильтр на IPv4 UDP-сокете (AF_INET, SOCK_DGRAM) подключается через setsockopt(SO_ATTACH_FILTER) и не требует bpf(2). По контракту сокетного интерфейса Linux ноль из фильтра означает «отбросить пакет», поэтому достаточно программы, которая всегда возвращает 0. Тест сначала отправляет датаграмму без фильтра, затем с ним.
Linux, root:
baseline, no filter: datagram RECEIVED
setsockopt(SO_ATTACH_FILTER, ret #0) ret=0 errno=0 (-)
after classic drop-all cBPF: datagram DROPPED -> EFFECT
gVisor 20260831.0, OCI-запуск, --network=host, uid 65534, CapEff=0:
baseline, NO filter attached: datagram RECEIVED
setsockopt(SO_ATTACH_FILTER, classic) ret=0 errno=0 (-)
after classic drop-all cBPF: datagram RECEIVED -> NO EFFECT
Коды возврата совпадают, разница видна только на следующей отправке. Результат стабилен при --network=none (loopback внутри песочницы) и --network=host, при runsc do и OCI-запуске. Задача на поддержку сокетных фильтров в gVisor открыта с 2020 года. Для TCP в Netstack есть дополнительная проверка CAP_NET_ADMIN, так что вывод на другие типы сокетов не переносится.
Важная оговорка: такой фильтр влияет только на приём конкретным сокетом и не мешает открыть другой. Политикой сетевого доступа для инструмента он не является, и расхождение само по себе не означает выход из песочницы. Здесь это пример успешной настройки без эффекта у процесса без capability.
Как не обмануть себя собственным тестом
Один код ошибки мало что объясняет. EINVAL может означать и неподдерживаемую операцию, и кривые аргументы пробника. Для сокетного фильтра проверяли корректную программу и варианты с испорченными инструкциями: Linux испорченные отвергал, gVisor принимал всё. Сравнение корректного и испорченного входа помогает найти расхождение, хотя одинаковые ответы ещё ничего не доказывают — оба вызова могли остановиться раньше, например на проверке прав. В таких случаях выручают исходники реализации.
Порядок проверки для обвязки:
- Без ограничения проверяемое действие выполняется. Если файл не открывался ещё до настройки, последующий отказ ничего не доказывает.
- Настройка с корректными аргументами принимается.
- После настройки запрещённое действие перестаёт выполняться, разрешённое продолжает работать.
- Всё повторяется в целевой среде: правила ставятся с правами обвязки, действия выполняются с правами и настройками инструмента.
Нехватку прав на настройку фиксируйте отдельно от отсутствия реализации.
Приёмка для анализатора «только чтение»
В одноразовом каталоге лежит копия исходников. Сначала тест запускается с правами будущего анализатора без дополнительной политики и убеждается, что чтение и запись доступны. Если запись уже запрещена правами файлов или опциями монтирования, новый запрет этим опытом не проверить. Затем та же проверка идёт через обвязку:
| Действие | До ограничения | После ограничения |
|---|---|---|
| Прочитать файл | успех | успех |
| Записать в существующий файл | успех | отказ, содержимое не изменилось |
| Укоротить файл | успех | отказ, размер не изменился |
| Создать новый файл | успех | отказ |
| Удалить или переименовать | успех | отказ, файл на месте |
Детали, на которых обычно спотыкаются:
- для каждой попытки нужна свежая копия файлов;
- проверка должна дойти до нужной операции: отказ запустить shell ничего не говорит о запрете записи через shell;
- если анализатор порождает дочерние процессы, те же действия проверяются из них;
- обвязка не должна передавать инструменту уже открытые на запись файловые дескрипторы — наследование ограничений и поведение ранее открытых файлов для Landlock описаны в документации ядра.
Набор прогоняется при приёмке конфигурации и повторяется после смены ядра, рантайма, образа инструмента, правил, прав, монтирований, настроек seccomp и no_new_privs. При каждом рабочем запуске обвязка применяет проверенную политику и останавливается, если установка вернула ошибку. Успешный пробник в соседнем процессе анализатор не ограничивает.
В рассмотренных релизах gVisor вариант с Landlock отвалится уже на запросе ABI. Если запрет записи обязателен, анализатор не запускается, пока не выбран и не проверен другой механизм. Работа с урезанным набором ограничений допустима только как заранее оговорённый режим для необязательной защиты. Решение о запуске принимается по наблюдаемому поведению запрета в целевой конфигурации; версия рантайма и ret=0 от системного вызова для этого не годятся.