// заметки
Кто вызывает unshare: перехват бинаря через PATH
Команда падает и не называет виновника. Показываю приём: подменить бинарь логирующим скриптом через PATH и найти вызывающего без трассировки и прав root
Команда падает, но в сообщении нет имени процесса, который её вызвал. В такой ситуации помогает простой приём: подменить нужный бинарь логирующим скриптом и поставить его первым в PATH (список каталогов, где оболочка ищет команды). Разбираю на примере unshare, но метод работает для любой команды
Проблема: сообщение без виновника
Классический сценарий: direnv exec . в проекте с use flake падает с unshare: unshare failed: Операция не позволена. Ошибку печатает unshare, но кто именно его вызвал, не сказано. Это может быть nix-direnv (плагин direnv для окружений Nix), обёртка над nix, хук direnv или что-то ещё
Поиск по конфигам вручную может затянуться. Нужен способ поймать вызов в момент, когда он происходит
Ошибка называет инструмент, но не вызывающего
Почему strace не всегда подходит
strace (трассировщик системных вызовов) с флагом -e trace=execve покажет все запуски процессов, включая unshare. Минус в объёме: нужный вызов тонет в выводе, а настройки ptrace (механизм, которым трассировщик подключается к процессу) не всегда позволяют это сделать. Для быстрого поиска достаточно перехватить одну команду
Причина: перехват через PATH
Оболочка ищет команду по каталогам PATH слева направо и запускает первый исполняемый файл с подходящим именем. Поэтому каталог со своим скриптом в начале PATH получает вызов раньше настоящего бинаря. Приём понадобился потому, что ошибку печатает инструмент (unshare), а не тот, кто его вызвал. В разобранном случае виновником оказалась обёртка над nix, которая повторно поднимала namespace: nix-direnv: «unshare failed: Операция не позволена»
Приём: подменяем бинарь
PATH резолвит команду по имени: оболочка берёт первый исполняемый файл с подходящим именем. Значит, достаточно положить свой скрипт unshare в отдельный каталог и поставить этот каталог в начало PATH
Все команды ниже выполняются в одном терминале
HOOKDIR="$(mktemp -d)"
cat > "$HOOKDIR/unshare" <<EOF
#!/bin/sh
echo "UNSHARE CALLED: \$@" >> "$HOOKDIR/unshare.log"
exit 1
EOF
chmod +x "$HOOKDIR/unshare"
export PATH="$HOOKDIR:$PATH"
direnv exec . true
cat "$HOOKDIR/unshare.log"Кавычки вокруг EOF не ставим: тогда $HOOKDIR подставится при создании скрипта, и в лог попадёт абсолютный путь. \$@ экранируем, чтобы он остался в скрипте и раскрылся уже при вызове
Скрипт пишет аргументы в лог и завершается с exit 1. Это имитация сбоя, а не сам исходный EPERM: вызов не уходит дальше по цепочке, а лог фиксирует, кто и с какими аргументами его сделал
После отладки окружение возвращается в исходное состояние:
rm -rf "$HOOKDIR"Проверка: читаем лог
Сразу оговорюсь: лог покажет аргументы вызова, но не имя процесса. Вызывающего ищем следующим шагом по своим каталогам
grep -rn "unshare" "$HOME"/.local/bin/ "$HOME"/.config/direnv/ 2>/dev/nullВ логе появится строка вида UNSHARE CALLED: --mount --map-root-user .... Аргументы показывают, как поднимается namespace (изолированное окружение уровня ядра). Сам факт вызова сужает круг: дальше ищем, кто мог вызвать unshare
В моём случае в логе оказалась собственная обёртка над nix, которая звала unshare повторно. Другой похожий случай, молчаливая подмена PATH из devShell в fish, разобран в заметке Nix devShell теряет пакеты в fish
Подтвердить виновника можно повторным запуском: уберите перехват, исправьте найденное место и повторите сбойный сценарий. Если ошибка unshare failed больше не появляется, вызов приходил именно оттуда
Подводные камни
- Ловит только вызовы по имени через
PATH. Команда, вызванная абсолютным путём (/usr/bin/unshare), подмену обойдёт - Не сработает, если имя занято shell-функцией или алиасом: оболочка выполнит их, а не бинарь из
PATH - Не увидит вызовы с очищенным окружением (
env -i) и запуски черезsudo: по умолчаниюsudoсбрасываетPATH(env_reset) - Не показывает уже запущенные процессы и не заменяет
straceили системный аудит
Итог
Приём не требует прав root: подменяем команду логирующим скриптом, ставим каталог первым в PATH, воспроизводим сбой и читаем лог. Когда виновник найден, правится конкретное место, а не всё окружение целиком
Похожие заметки
Nix devShell в fish: PATH молча не подхватывается
Пакеты из Nix devShell не подхватываются в fish: PATH указывает внутрь mount-namespace. Диагностика молчаливой подмены и прозрачный re-exec оболочки
nix-direnv: «unshare failed: Операция не позволена»
direnv с use flake падает на unshare failed, хотя dotenv работает. Разбираю причину (вложенный user-namespace) и показываю фикс обёртки над nix
nix-direnv: «minimum required bash version is 4.4»
nix-direnv падает с «minimum required bash version is 4.4», хотя bash новее. Разбираю вшитые пути /nix/store, почему текст врёт и как это исправить