1 điểm bởi GN⁺ 3 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Nix, một trình quản lý gói dựa trên store, được thiết kế để đặt các gói vào một prefix cố định như /nix/store, nên tạo ra nhiều ràng buộc trong môi trường rootless Nix muốn đặt store ở vị trí khác mà không cần cài Nix sẵn hoặc có quyền root
  • Nếu dùng --store /tmp/... cùng với chroot và mount namespace, có thể giữ nguyên cùng một hash như bản build /nix/store hiện có, nhờ đó vẫn tiếp tục tận dụng được binary cache như cache.nixos.org
  • Nếu đổi store prefix bằng local?store=/tmp/... mà không dùng namespace, hash sẽ thay đổi, và ngay cả một bản build hello đơn giản cũng có thể dẫn tới việc làm mất hiệu lực toàn bộ đồ thị phụ thuộc và biên dịch lại GCC
  • Cốt lõi của đề xuất là dùng đường dẫn tương đối dựa trên $ORIGIN mà trình liên kết động Linux hỗ trợ, thay vì đường dẫn tuyệt đối trong ELF RUNPATH, để việc thay đổi vị trí store không lan sang thay đổi hash và tái biên dịch
  • Nút thắt thực sự ngăn khả năng tái định vị là kernel không hỗ trợ $ORIGIN trong ELF PT_INTERP và shebang của script; các hướng giải quyết được đề xuất gồm vá kernel, wrapper tĩnh, đường dẫn tương đối theo từng ngôn ngữ, và metadata relocatable = true;

Xung đột giữa store prefix cố định và rootless Nix

  • Các hệ thống dựa trên store như Nix và Guix lưu mọi gói dưới một prefix đã định
    • Nix dùng /nix/store
    • Guix dùng /gnu/store
  • Cấu trúc này giúp việc viết lại đường dẫn binary hay thư viện trở nên dễ dàng
    • Ví dụ, /bin/bash có thể được thay bằng toàn bộ đường dẫn store như /nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
  • Cũng có những tình huống muốn đặt store ở vị trí khác
    • Môi trường chưa cài sẵn Nix
    • Môi trường không có đủ quyền cần thiết
    • Những trường hợp như vậy dẫn tới bài toán “rootless Nix”
  • Hiện tại Nix vẫn cho phép chỉ định đường dẫn store khác, nhưng tùy cách làm mà có giữ nguyên hash hay không
    • nix build nixpkgs#hello cài vào /nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/
    • nix build --store /tmp/fzakaria/store nixpkgs#hello dùng chroot và mount namespace để cài vào /tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/
    • Trong cả hai trường hợp, hash zi2bj2hlavv8q743li2s9diqbcpmrf9b đều giống nhau
  • Nếu hash giống nhau, có thể tận dụng các derivation được tính toán sẵn từ binary substituter như https://cache.nixos.org

Cái giá khi đổi store mà không dùng namespace

  • Các công cụ như Bazel hay Buck2 có thể đã dùng namespace sẵn cho sandbox riêng của chúng
    • Nếu tích hợp Nix vào các hệ sinh thái này, tính thực dụng sẽ giảm mạnh vì user namespace lồng nhau và các giới hạn mount
  • Vẫn có thể chỉ định store prefix thay thế mà không dùng chroot hay mount namespace, nhưng có nhược điểm là hash bị đổi
    • Ví dụ lệnh dùng --store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log'
    • Kết quả đường dẫn hello/tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3
    • Hash không còn là zi2... mà đổi thành qv3fhi1j9gh27fyds5n5b16yia8i6zn5
  • Chỉ một thay đổi đơn giản ở chuỗi store prefix cũng có thể làm mất hiệu lực dây chuyền của toàn bộ đồ thị phụ thuộc
    • Chỉ để in ra “Hello World” trong một thư mục khác mà có thể phải biên dịch GCC suốt 4 tiếng
    • Trong trường hợp này cũng không thể tận dụng cache công khai
  • Giới hạn này hiện cũng đã được nêu rõ trong tài liệu Nix

$ORIGIN giải quyết được gì và giới hạn còn lại ở kernel

  • Gốc rễ vấn đề là store prefix là một phần của chính derivation, nên ảnh hưởng tới việc tính hash
  • Nếu không dùng toàn bộ store prefix ở mọi nơi mà chuyển sang đường dẫn tương đối, có thể tránh được thay đổi hash
  • ELF RUNPATH là một trong những điểm có thể áp dụng
    • Ví dụ RUNPATH hiện tại của hello/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib
    • Loader của Linux hỗ trợ $ORIGIN, tức thư mục chứa file thực thi
    • Vì vậy có thể viết RUNPATH thành $ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib
    • Làm vậy thì dù vị trí store thay đổi, hash vẫn không đổi và cũng không cần tái biên dịch
  • Tuy nhiên trước khi dynamic linker đọc RUNPATH, kernel Linux phải tự tải chính dynamic linker đó trước
    • Đường dẫn này được lưu trong header PT_INTERP của ELF
    • Ví dụ là /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2
    • Hiện tại kernel Linux không hỗ trợ $ORIGIN trong PT_INTERP
  • Shebang của script cũng gặp cùng một giới hạn
    • Ví dụ #!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
    • Kernel kỳ vọng một đường dẫn tuyệt đối khi phân tích #!
    • Hiện tại shebang cũng không hỗ trợ $ORIGIN
  • Có thể dùng đường dẫn tương đối theo thư mục làm việc hiện tại, nhưng cách này dễ hỏng khi chạy script từ nơi khác nên không đáng tin cậy

Đề xuất hướng tới binary có thể tái định vị

  • Để tạo ra binary thật sự có thể tái định vị, cần lách qua hoặc thay đổi các giới hạn của kernel
  • Có ba hướng tiếp cận được đề xuất
    • Vá kernel Linux để hỗ trợ $ORIGIN trong PT_INTERP và shebang
    • Bọc mọi binary bằng một binary tĩnh nhỏ, để wrapper tự tính vị trí của nó rồi chạy dynamic linker
    • Thay đổi cả vị trí file để tận dụng khả năng đường dẫn tương đối theo từng ngôn ngữ
      • Trong Python, có thể dùng __file__ để truy cập file dựa trên chính vị trí của nó
  • Hướng được đề xuất là phù hợp nhất là mở rộng hỗ trợ trong kernel Linux
    • Trên máy NixOS, có thể vá kernel bằng Nix để thêm hỗ trợ này
  • Ngoài ra còn có đề xuất thêm metadata relocatable = true; cho từng derivation để biểu thị liệu nó có thể tái định vị hay không

1 bình luận

 
Ý kiến trên Lobste.rs
  • Sẽ rất tốt nếu nhân Linux hỗ trợ $ORIGIN trong PT_INTERP. Trước đây tôi từng thử bằng một binary wrapper tĩnh, và cũng đã thấy vài nỗ lực khác (ví dụ hay); tất cả đều là những pha hack rất xuất sắc và đẹp mắt, nhưng rốt cuộc vẫn chỉ là hack
    Tôi vẫn chưa thực sự hiểu rõ các hàm ý về bảo mật, nên nếu có một phần giải thích được hệ thống hóa thì sẽ rất hữu ích
    Có vẻ Solaris hỗ trợ điều này, nên có thể tồn tại cách làm an toàn. Khó tìm được bằng chứng rõ ràng, nhưng trong mục ENOEXEC của tài liệu execve(2) có nói rằng nếu program header PT_INTERP của file image tiến trình setuid/setgid dùng đường dẫn tương đối hoặc token $ORIGIN thì sẽ thất bại

  • Có phải rất nhiều phần mềm có đường dẫn được ghi cứng tại thời điểm build hoặc các hằng số, nên cuối cùng vẫn phải biên dịch lại thì mới chạy đúng không?

    • Trên thực tế đã có rất nhiều trường hợp như vậy, chủ yếu là ở dòng shebang. Nix có khá nhiều helper để thay thế các giá trị này tại thời điểm build
    • Đúng, nhưng vấn đề này vốn đã tồn tại từ trước và không liên quan đến local store. Có lẽ các derivation cấp dưới sẽ tiêu thụ outPath thông qua {foo}, nên nếu một trong các dependency cấp trên được đổi sang local store thì sẽ phải build lại
  • Bản vá dcrt1 cho musl (do rcombs viết) giải quyết vấn đề này ở không gian người dùng

  • Liệu có thể tạo một file system được mount tại /origin để nó được diễn giải thành $ORIGIN không? Như vậy có vẻ sẽ hoạt động cho cả shebang lẫn ELF mà không cần thêm cú pháp

    • Nếu đã có thể tạo và mount /origin, thì chẳng phải cũng có thể tạo luôn /nix và chạy nix-daemon sao?
    • Tôi nghĩ mục tiêu là để nó vẫn tương thích ngoài NixOS, tức là không cần cài đặt hay cấu hình thêm file system hoặc daemon nào
  • Nếu binary có thể chỉ định một loader đi kèm bằng đường dẫn tương đối, có thể là không an toàn, thì đó chẳng phải là một rủi ro bảo mật sao?

    • Trong mô hình đe dọa này, cái gì được tin cậy và cái gì không được tin cậy? Dù sao thì bạn cũng sẽ chạy binary đó, nên ý ở đây chỉ là muốn nó chạy bằng một loader đã được xác minh thôi sao?
    • Tại sao điều đó lại bị xem là kém an toàn hơn biến môi trường dùng để xác định đường dẫn tìm kiếm cho các thư viện liên kết động khác như libc.so.6?