// заметки
nix-direnv: «unshare failed: Операция не позволена»
direnv с use flake падает на unshare failed, хотя dotenv работает. Разбираю причину (вложенный user-namespace) и показываю фикс обёртки над nix
direnv exec . в проекте с use flake падает с unshare: unshare failed: Операция не позволена, хотя переменные из .env подхватываются. unshare создаёт пространства имён (namespace, изолированное окружение процессов), и сбой происходит на втором таком создании. Ниже: как найти причину и починить обёртку над nix
Контекст: rootless Nix, direnv и nix-direnv в проекте, где .envrc содержит use flake
Симптом
Воспроизводится внутри проекта с .envrc, где есть строка use flake
direnv: loading $HOME/projects/demo/.envrc
direnv: using flake
direnv: nix-direnv: cache invalidated: profile (…/.direnv/flake-profile-…) does not exist
unshare: unshare failed: Операция не позволена
direnv: nix-direnv: Evaluating current devShell failed. Falling back to previous environment!dotenv-часть при этом работает: простые переменные (APP_ENV и подобные) на месте, падает только ветка с flake
Диагностика
Первое, что сбивает с толку: unshare failed не говорит, какой процесс вызвал unshare (утилиту, создающую namespace). Поиск по конфигам direnv и $HOME/.local/bin быстро сужает круг, но точный ответ даёт перехват бинаря: unshare подменяется скриптом-логгером, каталог добавляется в PATH первым, и в логе появляется вызывающий. Приём разобран отдельно: Как узнать, кто вызывает unshare
Перехват показал:unshareзовёт моя же обёртка надnix. Она поднимает namespace и внутри уже поднятого пытается создать второй
Снаружи обёртка поднимает namespace №1. Внутри него nix-direnv вызывает nix, обёртка снова вызывает подъём namespace, и создание вложенного namespace падает
Хост, на котором это воспроизводилось, запрещает вложенные user-namespace на уровне политики. В Linux такие namespace в общем случае разрешены, так что это особенность конкретной системы
Причина
Разберём термины. User-namespace изолирует пользователей и их права, mount-namespace изолирует точки монтирования. Rootless Nix использует их, чтобы смонтировать /nix без прав root. Вложенный user-namespace создаётся внутри уже существующего
Первый unshare --mount --map-root-user проходит: создаются user- и mount-namespace, пользователь маппится в root. Второй, изнутри namespace №1, ядро отклоняет с EPERM. Политику хоста можно проверить так:
sysctl user.max_user_namespaces
sysctl kernel.apparmor_restrict_unprivileged_userns
sysctl kernel.unprivileged_userns_cloneЗапрещают вложенные user-namespace нулевое значение user.max_user_namespaces или kernel.unprivileged_userns_clone и единица у kernel.apparmor_restrict_unprivileged_userns. Набор и наличие ключей зависят от дистрибутива и ядра: kernel.unprivileged_userns_clone есть в Debian и производных, kernel.apparmor_restrict_unprivileged_userns только на системах с AppArmor, user.max_user_namespaces общий счётчик ядра. На проверенной машине, например, ключа kernel.apparmor_restrict_unprivileged_userns в /proc/sys не было, и sysctl вернул ошибку «cannot stat ... No such file or directory»
Любая обёртка, которая безусловно поднимает namespace, внутри него ломается. Это и есть корневая причина
Похожая, но другая проблема: когда PATH из devShell молча не подхватывается в fish. Разбор: Nix devShell теряет пакеты в fish
Решение
Обёртка над nix должна быть namespace-aware: если /nix/store уже смонтирован, nix вызывается напрямую, без unshare
# $HOME/.local/bin/direnv-shims/nix
if [ -d /nix/store ]; then
# Мы уже внутри namespace — nix можно звать напрямую
for _d in "$HOME"/.nix/store/*-nix-[0-9]*; do
case "$_d" in *.drv) continue;; esac
[ -x "$_d/bin/nix" ] && exec "$_d/bin/nix" "$@"
done
fi
# Снаружи — поднимаем namespace
exec "$HOME/.local/bin/nix-enter" nix "$@"Что здесь происходит. Если store смонтирован ([ -d /nix/store ]), обёртка ищет установленный nix в $HOME/.nix/store и запускает первый найденный бинарник через exec. Каталоги *.drv пропускаются: это описания сборок, а не установленные пакеты. Если store не смонтирован, обёртка вызывает nix-enter и поднимает namespace
Имена nix-enter и direnv-shims/nix это пример, у вас обёртка может называться иначе. Важна логика: проверить /nix/store перед попыткой создать namespace
Второй шаг: запускать сам direnv целиком внутри namespace ($HOME/.local/bin/nix-enter-direnv). Так нужно потому, что nix-direnv использует вшитые абсолютные пути /nix/store/..., которых снаружи не существует. Про вшитые пути и ложную ошибку версии bash: nix-direnv: «minimum required bash version is 4.4»
Проверка
Надёжный маркер «мы внутри namespace» только один: наличие смонтированного store
test -d /nix/store && echo внутри || echo снаружиНе полагайтесь на переменную $NIX_STORE_DIR: её можно выставить и снаружи, тогда проверка соврёт
Дальше пересоберите окружение с нуля:
rm -rf .direnv && direnv allowЕсли flake загружается без unshare failed, обёртка больше не создаёт вложенный namespace
Подводные камни
- Симптом врёт про виновника.
unshare failedне называет процесс; без перехвата бинаря поиск идёт вслепую - Пути `/nix/store/...` в `PATH` снаружи не работают молча:
command -v jqвернёт/usr/bin/jq. Поэтому внутри namespace стоит держать весь интерактивный шелл, а не толькоdirenv
Итог
unshare failed в связке direnv и nix-direnv означает, что обёртка над nix пытается создать второй user-namespace. Проверка /nix/store перед подъёмом namespace и запуск direnv внутри него убирают и ошибку, и ложную диагностику версий
Если ошибка осталась, проверьте, что обёртка не переопределяется в другом месте PATH: например, в $HOME/.local/bin лежит вторая копия или shell-функция с тем же именем
Похожие заметки
nix-direnv: «minimum required bash version is 4.4»
nix-direnv падает с «minimum required bash version is 4.4», хотя bash новее. Разбираю вшитые пути /nix/store, почему текст врёт и как это исправить
Nix devShell в fish: PATH молча не подхватывается
Пакеты из Nix devShell не подхватываются в fish: PATH указывает внутрь mount-namespace. Диагностика молчаливой подмены и прозрачный re-exec оболочки
Кто вызывает unshare: перехват бинаря через PATH
Команда падает и не называет виновника. Показываю приём: подменить бинарь логирующим скриптом через PATH и найти вызывающего без трассировки и прав root