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

> Clean Markdown view of GeekNews topic #30745. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30745](https://news.hada.io/topic?id=30745)
- GeekNews Markdown: [https://news.hada.io/topic/30745.md](https://news.hada.io/topic/30745.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-23T09:23:00+09:00
- Updated: 2026-06-23T09:23:00+09:00
- Original source: [fzakaria.com](https://fzakaria.com/2026/06/21/nix-needs-relocatable-binaries)
- Points: 1
- Comments: 1

## Topic Body

- **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](https://cache.nixos.org) 같은 **바이너리 substituter**의 사전 계산된 derivation을 활용할 수 있음

### namespace 없이 store를 바꿀 때의 비용
- [Bazel](https://bazel.build/)이나 [Buck2](https://buck2.build/) 같은 도구는 자체 샌드박싱을 위해 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 문서](https://nix.dev/manual/nix/2.24/store/types/local-store)에도 명시되어 있음

### `$ORIGIN`이 해결하는 부분과 남는 커널 한계
- 문제의 원인은 **store prefix가 derivation 자체의 일부**라서 해시 계산에 영향을 준다는 점임
- 전체 store prefix를 모든 곳에 쓰지 않고 상대 경로를 쓰면 해시 변화를 피할 수 있음
- ELF 바이너리의 `RUNPATH`는 적용 가능한 지점 중 하나임
  - 현재 `hello`의 `RUNPATH` 예시는 `/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib`
  - Linux 로더는 실행 파일이 있는 디렉터리를 뜻하는 [`$ORIGIN`](https://man7.org/linux/man-pages/man8/ld.so.8.html)을 지원함
  - 따라서 `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;` 메타데이터를 넣는 방안이 제안됨

## Comments



### Comment 60191

- Author: neo
- Created: 2026-06-23T09:23:01+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/pa1atu/nix_needs_relocatable_binaries) 
- Linux 커널에서 `PT_INTERP`에 **`$ORIGIN` 지원**이 들어가면 좋겠음. 예전에 정적 래퍼 바이너리로 해봤고, 다른 시도도 여럿 봤는데([좋은 예시](https://www.youtube.com/watch?v=RCcE5sxOn2o)), 전부 훌륭하고 멋진 해킹이지만 결국 해킹임  
  보안상 함의는 아직 제대로 이해하지 못했어서, 정리된 설명이 있으면 도움이 될 듯함  
  Solaris는 이를 지원하는 것 같은데, 안전하게 하는 방법이 있을지도 모름. 근거를 찾기 어렵지만 [`execve(2)`](https://docs.oracle.com/cd/E86824_01/html/E54765/execve-2.html) 매뉴얼의 ENOEXEC에는 setuid/setgid 프로세스 이미지 파일의 `PT_INTERP` 프로그램 헤더가 상대 경로를 갖거나 `$ORIGIN` 토큰을 쓰면 실패한다고 되어 있음
  - 참고가 될 만한 자료들: https://engineering.backtrace.io/2016-06-29-exploiting-elf-expansion-variables/ 와 https://illumos.topicbox.com/groups/developer/T40eb68883ef41616-Mb794149f7c6b10df7c1b319a/6987-disallow-setid-binaries-with-origin-in-ptinterp

- 많은 소프트웨어가 **빌드 시점에 박힌 경로**나 상수를 갖고 있어서, 제대로 동작하려면 결국 다시 컴파일해야 하지 않나?
  - 이미 그런 경우가 많고, 주로 **shebang 줄**에서 그렇다. Nix에는 빌드 시점에 이런 값을 치환하는 도우미가 많이 들어 있음
  - 맞지만, 이 문제는 로컬 저장소와 무관하게 원래부터 있었음. 아마 하위 derivation들은 `{foo}`를 통해 `outPath`를 소비할 텐데, 상위 의존성 중 하나를 로컬 저장소로 바꾸면 다시 빌드해야 함

- musl용 **dcrt1 패치**(rcombs 작성)가 이 문제를 사용자 공간에서 해결함

- `/origin`에 마운트된 파일 시스템을 만들어서 `$ORIGIN`으로 해석되게 할 수는 없을까? 그러면 추가 문법 없이 shebang과 ELF에서 모두 동작할 듯함
  - `/origin`을 만들고 마운트할 수 있다면, 그냥 `/nix`를 만들고 `nix-daemon`을 실행할 수도 있지 않나?
  - 내 생각엔 NixOS 밖에서도 호환되게 하려면, 추가 파일 시스템이나 데몬 설치·설정 없이 가는 게 목표일 듯함

- 바이너리가 상대 경로로, 아마 안전하지 않고 자체 제공된 **로더**를 지정할 수 있다면 보안상 위험하지 않을까?
  - 이 위협 모델에서 무엇을 신뢰하고 무엇을 신뢰하지 않는 건가? 어차피 그 바이너리를 실행할 거라면, 단지 검증된 로더로 실행하겠다는 건가?
  - `libc.so.6` 같은 다른 동적 링크 라이브러리의 검색 경로를 정하는 **환경 변수**보다 왜 더 덜 안전하다고 봐야 하나?
