1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Nix ซึ่งเป็น ตัวจัดการแพ็กเกจแบบ store-based ถูกออกแบบให้วางแพ็กเกจไว้ใน prefix แบบตายตัวอย่าง /nix/store ทำให้มีข้อจำกัดมากในสภาพแวดล้อม rootless Nix ที่ต้องการวาง store ไว้คนละตำแหน่งโดยไม่มีการติดตั้ง Nix เดิมหรือสิทธิ์ root
  • หากใช้ --store /tmp/... ร่วมกับ chroot และ mount namespace ก็จะยังคงใช้แฮชเดียวกับการบิลด์บน /nix/store เดิมได้ จึงยังใช้ binary cache อย่าง cache.nixos.org ต่อไปได้
  • ถ้าเปลี่ยน store prefix เป็น local?store=/tmp/... โดยไม่ใช้ namespace แฮชจะเปลี่ยน และแม้แต่การบิลด์ hello ง่าย ๆ ก็อาจทำให้กราฟ dependency ทั้งหมดใช้ไม่ได้ต่อเนื่องไปจนถึงการคอมไพล์ GCC ใหม่
  • แกนของข้อเสนอคือให้ใช้พาธสัมพัทธ์ที่อิง $ORIGIN ซึ่ง Linux dynamic linker รองรับ แทนพาธสัมบูรณ์ใน ELF RUNPATH เพื่อไม่ให้การย้ายตำแหน่ง store ลุกลามไปเป็นการเปลี่ยนแฮชและการคอมไพล์ใหม่
  • คอขวดที่ขวางความสามารถในการย้ายตำแหน่งจริง ๆ คือเคอร์เนลยังไม่รองรับ $ORIGIN ใน ELF PT_INTERP และ shebang ของสคริปต์ โดยมีการเสนอแนวทางแก้ทั้งการแพตช์เคอร์เนล, static wrapper, พาธสัมพัทธ์เฉพาะภาษา และเมตาดาต้า relocatable = true;

การชนกันระหว่าง store prefix แบบตายตัวกับ rootless Nix

  • ระบบแบบ store-based อย่าง Nix และ Guix จะเก็บแพ็กเกจทั้งหมดไว้ใต้ prefix ที่กำหนดไว้แน่นอน
    • Nix ใช้ /nix/store
    • Guix ใช้ /gnu/store
  • โครงสร้างนี้ทำให้เขียนทับพาธของไบนารีหรือไลบรารีได้ง่าย
    • ตัวอย่างเช่นสามารถแทน /bin/bash ด้วยพาธ store เต็มอย่าง /nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash ได้
  • แต่ก็มีสถานการณ์ที่ต้องการวาง 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 เหมือนกัน
  • เมื่อแฮชเหมือนกัน ก็สามารถใช้ derivation ที่คำนวณไว้ล่วงหน้าจาก binary substituter อย่าง https://cache.nixos.org ได้

ต้นทุนของการเปลี่ยน store โดยไม่ใช้ namespace

  • เครื่องมืออย่าง Bazel หรือ Buck2 อาจใช้ namespace อยู่แล้วเพื่อทำ sandboxing ของตัวเอง
    • ถ้าจะรวม Nix เข้าไปใน ecosystem แบบนี้ ความสามารถใช้งานจริงจะลดลงมากเพราะข้อจำกัดของ nested user namespace และ mount
  • แม้จะกำหนด store prefix ทางเลือกได้โดยไม่ใช้ chroot และ mount namespace แต่มีข้อเสียคือแฮชจะเปลี่ยน
    • ตัวอย่างคำสั่งใช้ --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 สามารถทำให้ กราฟ dependency ทั้งหมด ใช้ไม่ได้เป็นลูกโซ่
    • แค่ต้องการให้ “Hello World” ทำงานจากอีกโฟลเดอร์หนึ่ง ก็อาจจบลงที่ต้องคอมไพล์ GCC นาน 4 ชั่วโมง
    • ในกรณีนี้จะใช้ public cache ไม่ได้
  • ข้อจำกัดนี้ถูกระบุไว้ชัดเจนใน เอกสารของ Nix แล้ว

สิ่งที่ $ORIGIN ช่วยได้ และข้อจำกัดของเคอร์เนลที่ยังเหลืออยู่

  • ต้นตอของปัญหาคือ store prefix เป็นส่วนหนึ่งของ derivation เอง จึงมีผลต่อการคำนวณแฮช
  • ถ้าไม่ใช้ store prefix เต็มทุกที่ แต่เปลี่ยนไปใช้พาธสัมพัทธ์ ก็จะเลี่ยงการเปลี่ยนแฮชได้
  • จุดหนึ่งที่ทำได้คือ RUNPATH ของไบนารี ELF
    • ตัวอย่าง RUNPATH ของ hello ปัจจุบันคือ /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib
    • Linux loader รองรับ $ORIGIN ซึ่งหมายถึงไดเรกทอรีที่ไฟล์ executable อยู่
    • ดังนั้นจึงเขียน RUNPATH เป็น $ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib ได้
    • แบบนี้แม้ตำแหน่งของ store จะเปลี่ยน ก็ไม่ทำให้แฮชเปลี่ยนและไม่ต้องคอมไพล์ใหม่
  • แต่ก่อนที่ dynamic linker จะอ่าน RUNPATH ได้ Linux kernel ต้องเป็นฝ่ายโหลด dynamic linker เองก่อน
    • พาธนี้ถูกเก็บไว้ใน header PT_INTERP ของ ELF
    • ตัวอย่างคือ /nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2
    • ปัจจุบัน Linux kernel ยังไม่รองรับ $ORIGIN ใน PT_INTERP
  • shebang ของสคริปต์ก็มีข้อจำกัดแบบเดียวกัน
    • ตัวอย่างคือ #!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
    • เคอร์เนลคาดหวังพาธแบบสัมบูรณ์เมื่อ parse #!
    • ปัจจุบัน shebang ก็ยังไม่รองรับ $ORIGIN
  • แม้จะใช้พาธสัมพัทธ์อิงจากไดเรกทอรีทำงานปัจจุบันได้ แต่ถ้ารันสคริปต์จากที่อื่นก็จะพัง จึงเชื่อถือได้ยาก

ข้อเสนอเพื่อมุ่งไปสู่ไบนารีที่ย้ายตำแหน่งได้

  • หากจะสร้าง ไบนารีที่ย้ายตำแหน่งได้จริง ก็ต้องหลบหรือเปลี่ยนข้อจำกัดของเคอร์เนลเหล่านี้
  • แนวทางที่เสนอมี 3 แบบ
    • แพตช์ Linux kernel ให้รองรับ $ORIGIN ใน PT_INTERP และ shebang
    • ครอบไบนารีทั้งหมดด้วยไบนารี static ขนาดเล็ก โดยให้ wrapper คำนวณตำแหน่งของตัวเองก่อนแล้วจึงเรียก dynamic linker
    • ปรับตำแหน่งไฟล์ให้ใช้ความสามารถพาธสัมพัทธ์เฉพาะภาษา
      • ใน Python สามารถใช้ __file__ เพื่อเข้าถึงไฟล์โดยอิงจากตำแหน่งของตัวเองได้
  • แนวทางที่ถูกเสนอว่าเหมาะสมที่สุดคือ การขยายการรองรับใน Linux kernel
    • บนเครื่อง NixOS สามารถใช้ Nix แพตช์เคอร์เนลเพื่อเพิ่มความสามารถนี้ได้
  • นอกจากนี้ยังมีข้อเสนอให้ใส่เมตาดาต้า relocatable = true; ในแต่ละ derivation เพื่อระบุว่าย้ายตำแหน่งได้หรือไม่

1 ความคิดเห็น

 
GN⁺ 3 시간 전
ความเห็นจาก Lobste.rs
  • น่าจะดีถ้าในเคอร์เนล Linux มี การรองรับ $ORIGIN ใน PT_INTERP เคยลองทำด้วยไบนารี wrapper แบบ static มาก่อน และก็เคยเห็นความพยายามอื่นอีกหลายแบบ(ตัวอย่างที่ดี) ซึ่งทั้งหมดก็เป็นแฮ็กที่ยอดเยี่ยมและชาญฉลาด แต่สุดท้ายก็ยังเป็นแฮ็กอยู่ดี
    ผม/ฉันยังไม่เข้าใจนัยด้านความปลอดภัยของเรื่องนี้ดีนัก เลยคิดว่าถ้ามีคำอธิบายที่เรียบเรียงไว้ก็น่าจะช่วยได้
    ดูเหมือน Solaris จะรองรับสิ่งนี้ อาจมีวิธีทำให้ปลอดภัยก็ได้ หาแหล่งอ้างอิงชัด ๆ ยาก แต่ใน ENOEXEC ของคู่มือ execve(2) ระบุว่า ถ้า program header PT_INTERP ของไฟล์อิมเมจโปรเซส setuid/setgid มีพาธแบบสัมพัทธ์หรือใช้โทเค็น $ORIGIN ก็จะล้มเหลว

  • ซอฟต์แวร์จำนวนมากมี พาธหรือค่าคงที่ที่ฝังไว้ตอน build ดังนั้นสุดท้ายก็ต้องคอมไพล์ใหม่เพื่อให้มันทำงานได้ถูกต้องไม่ใช่หรือ?

    • ตอนนี้ก็เป็นแบบนั้นอยู่บ่อยแล้ว โดยเฉพาะใน บรรทัด shebang และ Nix ก็มีตัวช่วยมากมายสำหรับแทนค่าพวกนี้ตอน build
    • จริง แต่ปัญหานี้เดิมทีก็มีอยู่แล้วโดยไม่เกี่ยวกับ local store น่าจะเป็นไปได้ว่า derivation ชั้นล่างจะใช้ outPath ผ่าน {foo} แต่ถ้าเปลี่ยนหนึ่งใน dependency ชั้นบนให้เป็น local store ก็ต้อง build ใหม่
  • แพตช์ dcrt1 สำหรับ musl (เขียนโดย rcombs) แก้ปัญหานี้ใน user space

  • จะสร้างไฟล์ซิสเต็มที่เมานต์ไว้ที่ /origin แล้วให้มันถูกตีความเป็น $ORIGIN ได้ไหม? ถ้าได้ แบบนั้นน่าจะใช้ได้ทั้งกับ shebang และ ELF โดยไม่ต้องเพิ่มไวยากรณ์อะไร

    • ถ้าสร้างและเมานต์ /origin ได้ ก็อาจจะสร้าง /nix แล้วรัน nix-daemon ไปเลยไม่ใช่หรือ?
    • ผม/ฉันคิดว่าเป้าหมายน่าจะเป็นการให้ใช้งานร่วมกันได้แม้อยู่นอก NixOS โดยไม่ต้องติดตั้งหรือตั้งค่าไฟล์ซิสเต็มหรือเดมอนเพิ่มเติม
  • ถ้าไบนารีสามารถระบุ loader ที่จัดมาเองผ่านพาธสัมพัทธ์ได้ ซึ่งอาจไม่ปลอดภัย แบบนั้นจะเป็นความเสี่ยงด้านความปลอดภัยหรือเปล่า?

    • ใน threat model นี้ อะไรคือสิ่งที่เชื่อถือได้และอะไรที่ไม่เชื่อถือ? ถ้ายังไงก็จะรันไบนารีนั้นอยู่แล้ว ประเด็นคือแค่อยากให้มันรันผ่าน loader ที่ผ่านการตรวจสอบใช่ไหม?
    • ทำไมสิ่งนี้ถึงควรถูกมองว่าปลอดภัยน้อยกว่า ตัวแปรสภาพแวดล้อม ที่ใช้กำหนด search path ของไลบรารี dynamic link อื่น ๆ อย่าง libc.so.6?