15 คะแนน โดย xguru 2023-11-23 | 3 ความคิดเห็น | แชร์ทาง WhatsApp
  • ฟังก์ชัน setenv() และ unsetenv() ของภาษา C ไม่สามารถใช้งานได้อย่างปลอดภัยในโปรแกรมที่ใช้เธรด
  • ฟังก์ชันเหล่านี้แก้ไขสถานะแบบโกลบอล และอาจทำให้เกิดการชนกันเมื่ออีกเธรดหนึ่งเรียก getenv()
  • ปัญหาการชนกันนี้ยังเกิดในภาษาอื่นที่ใช้ฟังก์ชันจากไลบรารีมาตรฐานของ C เช่น os.Setenv ของ Go และ std::env::set_var() ของ Rust
  • ในโปรแกรม Go ใช้เวลา 2 วันในการไล่ปัญหาและรายงานบั๊กที่เกี่ยวข้อง
    • เพราะตัว DNS resolver ภายในของ Go ใช้ getaddrinfo() และมันเรียก getenv()
  • แต่ปัญหานี้มีมานานมากแล้ว มีบทความเกี่ยวกับเรื่องนี้ตั้งแต่ปี 2017 และด้านล่างบทความยังเขียนว่า “เจอกันใหม่ในอีก 5 ปี 2022!” แต่สุดท้ายก็กลับมาเจอกันอีกในปี 2023
  • นี่เป็นข้อบกพร่องของมาตรฐาน POSIX ที่ขยายมาตรฐาน C ให้สามารถแก้ไขตัวแปรสภาพแวดล้อมได้
    • ส่วนที่น่าหงุดหงิดที่สุดคือ หลายคนที่มีอิทธิพลต่อมาตรฐานหรือดูแลไลบรารี C ไม่ได้มองว่านี่เป็นปัญหา
    • เหตุผลคือในสเปกระบุไว้อย่างชัดเจนอยู่แล้วว่าไม่สามารถใช้ setenv() ร่วมกับเธรดได้
    • ดังนั้นถ้าใครทำแล้วโปรแกรมชน ก็ถือว่าเป็นความผิดของคนนั้น
  • เพราะงั้นเราก็ควรจะ “อ่านสเปกของทุกฟังก์ชันอย่างละเอียด, ไม่ใช้ซอฟต์แวร์ที่คนอื่นเขียน, และไม่ใช้เธรด”
    • แต่ในซอฟต์แวร์ยุคใหม่ นี่เป็นสมมติฐานที่ไม่สมจริง
    • แทนที่จะเป็นแบบนั้น ควรพยายามสร้าง API ที่พังได้ยากกว่า และพัฒนาไปตามการเปลี่ยนแปลงของ ecosystem
  • ภาษา C และไลบรารีมาตรฐานยังคงมีบทบาทสำคัญในฐานรากของซอฟต์แวร์ส่วนใหญ่ ดังนั้นเราควรหาวิธีปรับปรุงมัน หรือไม่ก็หาวิธีเลิกใช้มัน

ทำไม setenv() ถึงไม่เป็น Thread-Safe

  • getenv() คืนค่าเป็น char* และแอปพลิเคชันไม่จำเป็นต้อง free ทีหลัง
  • ขณะที่เธรดหนึ่งกำลังใช้พอยน์เตอร์นี้ อีกเธรดหนึ่งอาจใช้ setenv() หรือ unsetenv() เพื่อเปลี่ยนตัวแปรสภาพแวดล้อมตัวเดิมได้
  • มาตรฐาน C มีเพียง getenv() เท่านั้น แต่ implementation ส่วนใหญ่ปฏิบัติตามมาตรฐาน POSIX ซึ่งมีฟังก์ชันสำหรับแก้ไข environment ด้วย
  • putenv() จะเพิ่ม char* เข้าไปในชุดตัวแปรสภาพแวดล้อม และถ้าแอปพลิเคชันแก้ไขหน่วยความจำหลังจาก putenv() คืนค่าแล้ว ตัวแปรสภาพแวดล้อมก็จะถูกแก้ไขตามไปด้วย
  • environ คืออาร์เรย์ของพอยน์เตอร์ที่ลงท้ายด้วย NULL (char**) ซึ่งแอปพลิเคชันสามารถอ่านและกำหนดค่าได้ และการเข้าถึงอาร์เรย์นี้ไม่ปลอดภัยต่อเธรด

วิธีที่ environment variables ถูก implement

  • เมื่อแอปพลิเคชันเขียนทับตัวแปรเดิม implementation ต้องตัดสินใจว่าจะจัดการอย่างไร
  • glibc และ Solaris/Illumos จะไม่ free ตัวแปรสภาพแวดล้อมเลย ดังนั้นค่าที่คืนจาก getenv() จะไม่เปลี่ยนและสามารถใช้ได้อย่างปลอดภัยระหว่างเธรด
  • musl และ FreeBSD/Apple จะ free ตัวแปรสภาพแวดล้อม ดังนั้นถ้าอีกเธรดหนึ่งเรียก setenv() แล้วนำพอยน์เตอร์ที่ได้จาก getenv() ไปใช้ อาจทำให้โปรแกรมชนได้
  • การรับประกันว่าชุดตัวแปรสภาพแวดล้อมจะถูกอัปเดตแบบ thread-safe เป็นปัญหาที่สอง และนี่เองที่ทำให้เกิดการชนใน glibc

ทำไมโปรแกรมถึงใช้ตัวแปรสภาพแวดล้อม

  • ตัวแปรสภาพแวดล้อมมีประโยชน์ในการตั้งค่า shared library หรือ language runtime ที่ถูกรวมอยู่ในโปรแกรมอื่น
  • ผู้ใช้สามารถเปลี่ยนการตั้งค่าได้โดยไม่ต้องให้ผู้เขียนโปรแกรมส่งค่าคอนฟิกมาอย่างชัดเจน
  • ไลบรารีจำนวนมากเรียก getenv() และโปรแกรมก็จำเป็นต้องเปลี่ยนค่าตัวแปรเหล่านี้เพื่อคอนฟิกไลบรารีที่ใช้งาน

ปัญหานี้ควรถูกแก้ และสามารถทำได้ดังนี้

  • สำหรับผม การที่ปัญหานี้เป็นที่รู้กันมานานแล้วถือเป็นเรื่องเหลือเชื่อ
  • มีการเสียเวลาหลายพันชั่วโมงไปกับการดีบักปัญหานี้หรือถกเถียงเรื่องวิธีหลบเลี่ยงมัน
  • วิธีแก้ปัญหา
    • สร้าง implementation ที่ปลอดภัยต่อเธรดแบบ Illumos/Solaris
      • วิธีนี้ยังมีข้อจำกัดอยู่บ้าง หน่วยความจำอาจรั่วใน setenv() และถ้าโปรแกรมใช้ putenv() หรือจัดการ environment โดยตรงก็ยังไม่ปลอดภัยอยู่ดี
      • แต่ก็ยังดีกว่า implementation ปัจจุบันของ Linux และ Apple
    • อีกทางหนึ่งคือเพิ่ม API ใหม่สำหรับดึงตัวแปรสภาพแวดล้อมทั้งหมดที่ปลอดภัยต่อเธรดตั้งแต่ระดับการออกแบบ เช่น getenv_s() ของ Microsoft
  • วิธีแก้ที่ผมชอบคือใช้ทั้งสองแนวทางร่วมกัน
    • ช่วยลดโอกาสเกิดปัญหากับโปรแกรมและไลบรารีเดิม พร้อมทั้งเปิดทางให้โค้ดหรือภาษาใหม่อย่าง Go และ Rust หลีกเลี่ยงปัญหานี้ได้อย่างสมบูรณ์
    • เพิ่มฟังก์ชันที่คล้าย getenv_s() เพื่อคัดลอกตัวแปรสภาพแวดล้อมหนึ่งตัวลงในบัฟเฟอร์ที่ผู้ใช้กำหนด
    • เพิ่ม API แบบ thread-safe สำหรับวนดูตัวแปรสภาพแวดล้อมทั้งหมด หรือคัดลอกตัวแปรทั้งหมด
    • ทำเครื่องหมายว่า getenv() เลิกแนะนำให้ใช้แล้ว และแนะนำฟังก์ชัน getenv() แบบใหม่ที่ปลอดภัยต่อเธรดแทน
    • ทำเครื่องหมายว่า putenv() เลิกแนะนำให้ใช้แล้ว และแนะนำ setenv() แทน
    • ทำเครื่องหมายว่า environ เลิกแนะนำให้ใช้แล้ว และแนะนำให้ใช้ฟังก์ชันจัดการตัวแปรสภาพแวดล้อมแทน
    • อัปเดต implementation ของตัวแปรสภาพแวดล้อมให้ปลอดภัยต่อเธรด

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

 
ahwjdekf 2023-11-24

"เพราะสเปกระบุไว้อย่างชัดเจนว่าไม่สามารถใช้ setenv() ร่วมกับเธรดได้" ==> เวลาจะใช้ API หรือ SDK การตรวจสอบข้อกำหนดใน specification อย่างละเอียดถือเป็นพื้นฐานที่สุดที่ต้องทำอยู่แล้ว ดูแล้วไม่ต่างจากการฝืนใช้เลยครับ

 
carnoxen 2025-01-24

ปัญหาคือไปใช้ฟีเจอร์ที่ถูกออกแบบมาผิดตั้งแต่แรก

 
cosine20 2023-11-27

Setenv ไม่ปลอดภัยต่อเธรด และ C ก็ไม่ต้องการแก้ไขเรื่องนี้

....