Перейти к содержимому
← Все заметки

// заметки

nix-direnv: «unshare failed: Операция не позволена»

direnv с use flake падает на unshare failed, хотя dotenv работает. Разбираю причину (вложенный user-namespace) и показываю фикс обёртки над nix

Опубликовано: 2026-10-084 мин чтения
Nixdirenvnix-direnvuser-namespaceLinux

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-функция с тем же именем

Нужно решить такую задачу на вашем проекте?