// заметки
Nix на CachyOS: установка невозможна — rootless-запуск
Nix не ставится, если корень read-only, sudo сломан, а путь /nix зашит в ELF. Разбор трёх ограничений и rootless-запуск через mount-namespace и chroot
На этом хосте 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-namespaceoverlayfs(один каталог поверх другого) с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
Похожие заметки
Nix rootless: сборка не стартует и store-хеш под GC
Локальная сборка падает с unable to start build process, а захардкоженный путь к nss-cacert удаляет nix-collect-garbage. Два фикса: sandbox и вычисление пути
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