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

// заметки

Nix на CachyOS: установка невозможна — rootless-запуск

Nix не ставится, если корень read-only, sudo сломан, а путь /nix зашит в ELF. Разбор трёх ограничений и rootless-запуск через mount-namespace и chroot

Опубликовано: 2026-10-176 мин чтения
NixCachyOSLinuxrootlesschroot

На этом хосте Nix не устанавливается: корень / смонтирован read-only, sudo сломан, а путь /nix/store зашит в ELF-интерпретатор (абсолютный путь к динамическому загрузчику внутри ELF). Штатная установка требует записи в /nix и root, поэтому ниже три ограничения и rootless-запуск (без root и без изменений в системе) через mount-namespace (изолированное дерево точек монтирования) и chroot (подмену корня для процесса)

Предусловие: готовый store (хранилище пакетов) уже лежит в ~/.nix. Перенос store — отдельный шаг, в статье он не рассматривается: нужен store, полученный на машине, где /nix доступен. Команды bootstrap здесь нет, потому что штатный установщик всё равно требует /nix и root

Симптом

Штатная установка останавливается на первом шаге: каталог /nix создать нельзя

$ mkdir -p /nix
mkdir: невозможно создать каталог «/nix»: Файловая система доступна только для чтения

Второй барьер — sudo: политика повышения привилегий сломана, и получить root не удаётся

$ sudo -n true
sudo: /etc/sudo.conf is owned by uid 65534, should be 0

Обходной путь «распаковать store в $HOME и запускать оттуда» упирается в третью ошибку

$ ~/.nix-profile/bin/nix --version
bash: .../nix: cannot execute: required file not found

Файл существует, права -rwxr-xr-x, ядро всё равно отказывается его исполнять

~/.nix-profile — профиль, каталог ссылок на установленные пакеты. Сами пакеты лежат в ~/.nix/store, а store — в ~/.nix, поэтому запуск из профиля всё равно ищет файлы по путям /nix/store, которых снаружи нет

Диагностика

На каждое ограничение — своя проверка

# 1) есть ли /nix и можно ли его создать
ls -ld /nix 2>&1; mkdir -p /nix 2>&1

# 2) работает ли sudo
sudo -n true 2>&1

# 3) куда смотрит ELF-интерпретатор
file ~/.nix-profile/bin/nix | tr ',' '\n' | grep interpreter

# 4) доступны ли непривилегированные user-namespace
unshare --mount --map-root-user echo OK
cat /proc/sys/user/max_user_namespaces
cat /proc/sys/kernel/unprivileged_userns_clone

# 5) минимальный chroot: где именно ломается запуск
unshare --mount --map-root-user bash -c '
  mount -t tmpfs tmpfs /mnt
  mkdir -p /mnt/usr /mnt/nix
  mount --rbind /usr /mnt/usr
  ln -sfn "$(readlink /lib64)" /mnt/lib64
  chroot /mnt /usr/bin/ls /'

Вывод file содержит строку вида interpreter /nix/store/<hash>-glibc-2.39-52/lib/ld-linux-x86-64.so.2 — это и есть причина отказа

Пятая проверка собирает минимальный новый корень и запускает в нём ls. Если она падает, дело не в Nix, а в отсутствующих корневых симлинках

Причина

Ограничения независимы, но складываются в один запрет на штатную установку

  • Корень / смонтирован read-only: создать /nix нельзя ни пользователю, ни процессу с uid 0 в namespace
  • sudo не работает: /etc/sudo.conf принадлежит nobody (uid 65534), политика повышения привилегий сломана. В managed-окружении такое ограничение согласуют с администратором, а не обходят
  • Путь /nix/store зашит в ELF-интерпретатор: ядро ищет динамический загрузчик по абсолютному адресу, а Nix-бинарники не релоцируемы

Третье ограничение объясняет, почему перенос store в $HOME сам по себе не помогает. Даже jq и bash из store не запустятся снаружи /nix, потому что ядро ищет интерпретатор по адресу из заголовка

Установщик Nix задаёт dest="/nix" и требует либо root для multi-user режима с демоном и группой nixbld (служебные пользователи-сборщики), либо запись в /nix для single-user. На этом хосте нет ни того, ни другого

Хост ведёт себя как контейнер: корень только для чтения, политика sudo сломана, а /nix на диске отсутствует

/nix существует только внутри mount-namespace и только на время выполнения команды. На диске этого каталога нет

Решение

Идея обёртки nix-enter (пример имени): собрать временный корень в tmpfs (файловой системе в памяти), подложить в него системные каталоги и ~/.nix, затем сделать chroot. Всё живёт в user-namespace (пространстве имён пользователей) и исчезает вместе с ним, диск не меняется

  • создать tmpfs во временном каталоге
  • сделать rbind (рекурсивный bind-mount) для usr, etc, var, run, dev, proc, sys, home, tmp, opt, srv, boot
  • пересоздать корневые симлинки bin, lib, lib64, sbin — без этого внутри chroot не запускается вообще ничего
  • смонтировать ~/.nix как /nix
  • выполнить chroot и запустить команду

Сокращённый рабочий фрагмент ~/.local/bin/nix-enter. Ниже три части одного файла, разделённые для разбора

Первая часть ищет пакет с самим nix в store и выбирает наибольшую версию

#!/usr/bin/env bash
set -euo pipefail

NIX_ROOT="${NIX_ROOT:-$HOME/.nix}"
[ -d "$NIX_ROOT/store" ] || { echo "нет store в $NIX_ROOT" >&2; exit 1; }

NIX_PKG=""
for d in "$NIX_ROOT"/store/*-nix-[0-9]*; do
    [ -x "$d/bin/nix" ] || continue
    case "$d" in *.drv) continue ;; esac
    if [ -z "$NIX_PKG" ]; then
        NIX_PKG="$d"
    else
        NIX_PKG="$(printf '%s\n%s\n' "$NIX_PKG" "$d" | sort -V | tail -1)"
    fi
done
[ -n "$NIX_PKG" ] || { echo "nix не найден в $NIX_ROOT/store" >&2; exit 1; }

Цикл отбрасывает .drv-записи и проверяет, что bin/nix исполняемый. sort -V сравнивает версии как числа, поэтому 2.24.10 не проигрывает 2.9

Вторая часть создаёт временный корень и запускает namespace, внутри которого монтируются системные каталоги

CALLER_PWD="$PWD"
TMPROOT="$(mktemp -d "${TMPDIR:-/tmp}/nix-enter.XXXXXX")"

cleanup() {
    mountpoint -q "$TMPROOT" && umount -R "$TMPROOT"
    rm -rf --one-file-system "$TMPROOT"
}
trap cleanup EXIT

unshare --mount --map-root-user -- bash -c '
set -euo pipefail
TMPROOT="$1"; NIX_ROOT="$2"; NIX_PKG="$3"; CALLER_PWD="$4"; shift 4

mount -t tmpfs tmpfs "$TMPROOT"

for d in usr etc var run dev proc sys home tmp opt srv boot; do
    [ -e "/$d" ] || continue
    mkdir -p "$TMPROOT/$d"
    mount --rbind "/$d" "$TMPROOT/$d"
done

for l in bin lib lib64 sbin; do
    [ -L "/$l" ] || continue
    ln -sfn "$(readlink "/$l")" "$TMPROOT/$l"
done

mkdir -p "$TMPROOT/nix"
mount --bind "$NIX_ROOT" "$TMPROOT/nix"

unshare --mount --map-root-user создаёт новый mount-namespace и uid 0 внутри него. rbind переносит каталоги вместе с вложенными монтированиями. Корневые симлинки пересоздаются до chroot: иначе /usr/bin/ls не найдёт lib64, а сообщение будет тем же required file not found

Третья часть собирает окружение и делает chroot

NIX_BIN="/nix/${NIX_PKG#"$NIX_ROOT"/}/bin"
exec chroot "$TMPROOT" /usr/bin/env \
    HOME="$HOME" \
    USER="${USER:-$(id -un)}" \
    LOGNAME="${LOGNAME:-${USER:-$(id -un)}}" \
    TERM="${TERM:-xterm-256color}" \
    PATH="$NIX_BIN:/usr/local/bin:/usr/bin:/bin" \
    NIX_STORE_DIR=/nix/store \
    NIX_STATE_DIR=/nix/var/nix \
    NIX_CONF_DIR=/nix/etc/nix \
    NIX_REMOTE= \
    NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
    /usr/bin/bash -c "cd \"\$1\" 2>/dev/null || cd /; shift; exec \"\$@\"" \
    _ "$CALLER_PWD" "$@"
' -- "$TMPROOT" "$NIX_ROOT" "$NIX_PKG" "$CALLER_PWD" "$@"

NIX_CONF_DIR указывает на nix.conf внутри store: без него Nix не прочитает настройки, включая sandbox = false. CALLER_PWD сохраняет каталог вызова: после chroot прежний рабочий каталог недоступен, поэтому его восстанавливают до запуска команды. Подстановка ${USER:-$(id -un)} нужна из-за set -u: без неё неинтерактивный запуск без USER упадёт

Два bash -c решают разные задачи. Внешний запускает тело в namespace, где возможны монтажи. Внутренний, уже после chroot, выполняет пользовательскую команду. Аргумент _ занимает позицию $0, поэтому первый аргумент остаётся в $1

cleanup проверяет, смонтирован ли TMPROOT, и размонтирует рекурсивно только его. rm -rf --one-file-system не переходит границы файловых систем, поэтому не может пройтись по rbind-копиям /home и /etc

nix-enter — не песочница и не повышение привилегий: uid 0 существует только в namespace, а /home, /etc и сеть проброшены как есть. Недоверенный код так не запускайте

Почему не overlay и не pivot_root

  • pivot_root в tmpfs: после переключения /usr/bin/ls перестаёт запускаться, если не пересоздать симлинки lib и lib64; chroot проще и решает задачу
  • mount -o remount,rw на rbind корня: mount: доступ запрещён, флаг read-only не снимается в user-namespace
  • overlayfs (один каталог поверх другого) с lowerdir=/ (нижний слой — корень /): wrong fs type, bad option, bad superblock; overlay поверх ro-корня в непривилегированном namespace не поднимается, хотя overlay на tmpfs работает
  • bind-mount поверх существующего каталога: /nix нет среди существующих, создать его нельзя

Проверка

Первая проверка показывает, что /nix виден только внутри обёртки

$ ls -d /nix/store
ls: невозможно получить доступ: Нет такого файла или каталога
$ nix-enter ls -d /nix/store
/nix/store

Вторая — что nix запускается и видит store

$ nix-enter nix --version
nix (Nix) 2.24.10
$ nix-enter bash -c 'test -d /nix/store && echo внутри'
внутри

Третья — что диск не изменился: после выхода из namespace /nix снова отсутствует, а findmnt не показывает новых монтирований

$ test -d /nix/store || echo снаружи
снаружи
$ findmnt -rno TARGET | grep -x /nix || echo нет
нет

Если оболочка — fish, перезапуск внутри namespace описан отдельно: Nix devShell в fish: PATH молча не подхватывается

Для случая, когда direnv пытается поднять вложенный namespace, есть отдельный разбор: nix-direnv: «unshare failed: Операция не позволена»

Подводные камни

  • lib и lib64 — не каталоги, а симлинки на usr/lib. Их нужно пересоздать явно, иначе chroot не запустит ни один бинарник, а ошибка будет той же: required file not found при существующем файле
  • Профиль Nix ненадёжен: nix profile install <пакет> перезаписывает default, и оттуда может исчезнуть сам nix. Ищите пакет в store глобом и берите наибольшую версию, как во фрагменте
  • unshare --map-root-user даёт uid 0, но не снимает read-only с корня: это разные механизмы
  • Внутри namespace ls -la показывает uid 0 у файлов — следствие маппинга, а не ошибка
  • Namespace эфемерный: после выхода /nix исчезает, а GUI-редакторы и фоновые процессы, запущенные снаружи, Nix не увидят
  • Если непривилегированные user-namespace запрещены (max_user_namespaces = 0 или unprivileged_userns_clone = 0), обёртка не заработает: unshare завершится ошибкой, и обойти это без администратора нельзя
  • Путь к CA-сертификатам лучше вычислять в store, а не хардкодить хеш, который может удалить сборщик мусора. Фрагмент использует системный /etc/ssl/certs/ca-certificates.crt как запасной вариант

Итог

В окружении с read-only корнем и сломанным sudo штатная установка Nix невозможна: мешают три независимых ограничения, и главное из них — зашитый в ELF путь /nix/store. Rootless-запуск через mount-namespace и chroot обходит все три, не меняя диск, но требует готового store в ~/.nix

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