Nix에는 재배치 가능한 바이너리가 필요함

2 hours ago 1
  • store 기반 패키지 관리자인 Nix는 /nix/store 같은 고정 prefix에 패키지를 두도록 설계되어, 기존 Nix 설치나 root 권한 없이 다른 위치에 store를 두려는 rootless Nix 환경에서 제약이 커짐
  • --store /tmp/...와 chroot·mount namespace를 함께 쓰면 기존 /nix/store 빌드와 같은 해시를 유지해 cache.nixos.org 같은 바이너리 캐시를 계속 활용할 수 있음
  • namespace 없이 local?store=/tmp/...로 store prefix를 바꾸면 해시가 달라지고, 단순한 hello 빌드도 의존성 그래프 전체 무효화와 GCC 재컴파일로 이어질 수 있음
  • 제안의 핵심은 ELF RUNPATH에 절대 경로 대신 Linux 동적 링커가 지원하는 $ORIGIN 기반 상대 경로를 써서 store 위치 변경이 해시와 재컴파일로 번지지 않게 하는 것임
  • 실제 재배치 가능성을 막는 병목은 커널이 ELF PT_INTERP와 스크립트 shebang에서 $ORIGIN을 지원하지 않는 점이며, 커널 패치·정적 래퍼·언어별 상대 경로·relocatable = true; 메타데이터가 해결 방향으로 제안됨

고정 store prefix와 rootless Nix의 충돌

  • Nix와 Guix 같은 store 기반 시스템은 모든 패키지를 정해진 prefix 아래 저장함
    • Nix는 /nix/store
    • Guix는 /gnu/store
  • 이 구조에서는 바이너리나 라이브러리 경로를 재작성하기 쉬움
    • 예를 들어 /bin/bash를 /nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash 같은 전체 store 경로로 바꿀 수 있음
  • 다른 위치에 store를 두고 싶은 상황도 있음
    • Nix가 이미 설치되어 있지 않은 환경
    • 필요한 권한이 없는 환경
    • 이런 경우가 “rootless Nix” 문제로 이어짐
  • Nix는 현재도 다른 store 경로를 지정할 수 있지만, 방식에 따라 해시 유지 여부가 갈림
    • nix build nixpkgs#hello는 /nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/에 설치함
    • nix build --store /tmp/fzakaria/store nixpkgs#hello는 chroot와 mount namespace를 사용해 /tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/에 설치함
    • 두 경우 모두 해시 zi2bj2hlavv8q743li2s9diqbcpmrf9b가 같음
  • 해시가 같으면 https://cache.nixos.org 같은 바이너리 substituter의 사전 계산된 derivation을 활용할 수 있음

namespace 없이 store를 바꿀 때의 비용

  • Bazel이나 Buck2 같은 도구는 자체 샌드박싱을 위해 namespace를 이미 사용할 가능성이 있음
    • 이런 생태계에 Nix를 통합하려 하면 중첩 user namespace와 mount 제한 때문에 실용성이 크게 떨어짐
  • chroot와 mount namespace 없이도 대체 store prefix를 지정할 수는 있지만, 해시가 달라지는 결함이 있음
    • 예시 명령은 --store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log'를 사용함
    • 결과 hello 경로는 /tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3
    • 해시는 기존 zi2...가 아니라 qv3fhi1j9gh27fyds5n5b16yia8i6zn5로 바뀜
  • 단순한 store prefix 문자열 변경이 의존성 그래프 전체를 연쇄 무효화함
    • 다른 폴더에서 “Hello World”를 출력하려는 것만으로 GCC를 4시간 컴파일하게 될 수 있음
    • 이 경우 공개 캐시를 활용할 수 없음
  • 이 한계는 현재 Nix 문서에도 명시되어 있음

$ORIGIN이 해결하는 부분과 남는 커널 한계

  • 문제의 원인은 store prefix가 derivation 자체의 일부라서 해시 계산에 영향을 준다는 점임
  • 전체 store prefix를 모든 곳에 쓰지 않고 상대 경로를 쓰면 해시 변화를 피할 수 있음
  • ELF 바이너리의 RUNPATH는 적용 가능한 지점 중 하나임
    • 현재 hello의 RUNPATH 예시는 /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib
    • Linux 로더는 실행 파일이 있는 디렉터리를 뜻하는 $ORIGIN을 지원함
    • 따라서 RUNPATH를 $ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib처럼 쓸 수 있음
    • 이렇게 하면 store 위치가 바뀌어도 해시가 바뀌지 않고 재컴파일도 필요 없어짐
  • 하지만 동적 링커가 RUNPATH를 읽기 전에 Linux 커널이 먼저 동적 링커 자체를 로드해야 함
    • 이 경로는 ELF의 PT_INTERP 헤더에 저장됨
    • 예시는 /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2
    • 현재 Linux 커널은 PT_INTERP에서 $ORIGIN을 지원하지 않음
  • 스크립트 shebang도 같은 제약을 가짐
    • 예시는 #!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
    • 커널은 #!를 파싱할 때 절대 경로를 기대함
    • shebang에서도 현재 $ORIGIN 지원이 없음
  • 현재 작업 디렉터리 기준 상대 경로는 사용할 수 있지만, 다른 위치에서 스크립트를 실행하면 깨지므로 신뢰하기 어려움

재배치 가능한 바이너리로 가는 제안

  • 진정한 재배치 가능한 바이너리를 만들려면 커널 제약을 우회하거나 바꿔야 함
  • 제안된 접근은 세 가지임
    • Linux 커널을 패치해 PT_INTERP와 shebang에서 $ORIGIN을 지원함
    • 모든 바이너리를 작은 정적 바이너리로 감싸고, 래퍼가 자기 위치를 계산한 뒤 동적 링커를 실행함
    • 파일 위치도 언어별 상대 경로 기능을 활용하도록 바꿈
      • Python에서는 __file__로 자기 자신 기준 파일 접근을 할 수 있음
  • 가장 적합한 접근으로는 Linux 커널 지원 확장이 제안됨
    • NixOS 머신에서는 Nix로 커널을 패치해 해당 지원을 추가할 수 있음
  • 추가로 각 derivation에 재배치 가능 여부를 나타내는 relocatable = true; 메타데이터를 넣는 방안이 제안됨
Read Entire Article