- ฟังก์ชัน
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 ความคิดเห็น
"เพราะสเปกระบุไว้อย่างชัดเจนว่าไม่สามารถใช้
setenv()ร่วมกับเธรดได้" ==> เวลาจะใช้ API หรือ SDK การตรวจสอบข้อกำหนดใน specification อย่างละเอียดถือเป็นพื้นฐานที่สุดที่ต้องทำอยู่แล้ว ดูแล้วไม่ต่างจากการฝืนใช้เลยครับปัญหาคือไปใช้ฟีเจอร์ที่ถูกออกแบบมาผิดตั้งแต่แรก
Setenv ไม่ปลอดภัยต่อเธรด และ C ก็ไม่ต้องการแก้ไขเรื่องนี้
....