Nix에는 재배치 가능한 바이너리가 필요함
(fzakaria.com)- 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
- Nix는
- 이 구조에서는 바이너리나 라이브러리 경로를 재작성하기 쉬움
- 예를 들어
/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을 지원하지 않음
- 이 경로는 ELF의
- 스크립트 shebang도 같은 제약을 가짐
- 예시는
#!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash - 커널은
#!를 파싱할 때 절대 경로를 기대함 - shebang에서도 현재
$ORIGIN지원이 없음
- 예시는
- 현재 작업 디렉터리 기준 상대 경로는 사용할 수 있지만, 다른 위치에서 스크립트를 실행하면 깨지므로 신뢰하기 어려움
재배치 가능한 바이너리로 가는 제안
- 진정한 재배치 가능한 바이너리를 만들려면 커널 제약을 우회하거나 바꿔야 함
- 제안된 접근은 세 가지임
- Linux 커널을 패치해
PT_INTERP와 shebang에서$ORIGIN을 지원함 - 모든 바이너리를 작은 정적 바이너리로 감싸고, 래퍼가 자기 위치를 계산한 뒤 동적 링커를 실행함
- 파일 위치도 언어별 상대 경로 기능을 활용하도록 바꿈
- Python에서는
__file__로 자기 자신 기준 파일 접근을 할 수 있음
- Python에서는
- Linux 커널을 패치해
- 가장 적합한 접근으로는 Linux 커널 지원 확장이 제안됨
- NixOS 머신에서는 Nix로 커널을 패치해 해당 지원을 추가할 수 있음
- 추가로 각 derivation에 재배치 가능 여부를 나타내는
relocatable = true;메타데이터를 넣는 방안이 제안됨
댓글과 토론
Lobste.rs 의견들
-
Linux 커널에서
PT_INTERP에$ORIGIN지원이 들어가면 좋겠음. 예전에 정적 래퍼 바이너리로 해봤고, 다른 시도도 여럿 봤는데(좋은 예시), 전부 훌륭하고 멋진 해킹이지만 결국 해킹임
보안상 함의는 아직 제대로 이해하지 못했어서, 정리된 설명이 있으면 도움이 될 듯함
Solaris는 이를 지원하는 것 같은데, 안전하게 하는 방법이 있을지도 모름. 근거를 찾기 어렵지만execve(2)매뉴얼의 ENOEXEC에는 setuid/setgid 프로세스 이미지 파일의PT_INTERP프로그램 헤더가 상대 경로를 갖거나$ORIGIN토큰을 쓰면 실패한다고 되어 있음 -
많은 소프트웨어가 빌드 시점에 박힌 경로나 상수를 갖고 있어서, 제대로 동작하려면 결국 다시 컴파일해야 하지 않나?
- 이미 그런 경우가 많고, 주로 shebang 줄에서 그렇다. Nix에는 빌드 시점에 이런 값을 치환하는 도우미가 많이 들어 있음
- 맞지만, 이 문제는 로컬 저장소와 무관하게 원래부터 있었음. 아마 하위 derivation들은
{foo}를 통해outPath를 소비할 텐데, 상위 의존성 중 하나를 로컬 저장소로 바꾸면 다시 빌드해야 함
-
musl용 dcrt1 패치(rcombs 작성)가 이 문제를 사용자 공간에서 해결함
-
/origin에 마운트된 파일 시스템을 만들어서$ORIGIN으로 해석되게 할 수는 없을까? 그러면 추가 문법 없이 shebang과 ELF에서 모두 동작할 듯함/origin을 만들고 마운트할 수 있다면, 그냥/nix를 만들고nix-daemon을 실행할 수도 있지 않나?- 내 생각엔 NixOS 밖에서도 호환되게 하려면, 추가 파일 시스템이나 데몬 설치·설정 없이 가는 게 목표일 듯함
-
바이너리가 상대 경로로, 아마 안전하지 않고 자체 제공된 로더를 지정할 수 있다면 보안상 위험하지 않을까?
- 이 위협 모델에서 무엇을 신뢰하고 무엇을 신뢰하지 않는 건가? 어차피 그 바이너리를 실행할 거라면, 단지 검증된 로더로 실행하겠다는 건가?
libc.so.6같은 다른 동적 링크 라이브러리의 검색 경로를 정하는 환경 변수보다 왜 더 덜 안전하다고 봐야 하나?