15 điểm bởi xguru 2023-11-23 | 3 bình luận | Chia sẻ qua WhatsApp
  • Các hàm setenv()unsetenv() của ngôn ngữ C không thể được sử dụng an toàn trong các chương trình dùng luồng
  • Các hàm này sửa đổi trạng thái toàn cục và có thể gây xung đột khi một luồng khác gọi getenv()
  • Xung đột cũng xảy ra trong các ngôn ngữ khác dùng hàm thư viện chuẩn C, như os.Setenv của Go và std::env::set_var() của Rust
  • Đã mất 2 ngày để lần theo vấn đề liên quan và báo cáo lỗi trong một chương trình Go
    • Vì bộ phân giải DNS của Go dùng getaddrinfo() ở bên trong, mà hàm này lại gọi getenv()
  • Nhưng vấn đề này đã tồn tại từ rất lâu
    • Đã có một bài viết liên quan từ năm 2017, và ở cuối bài có câu “hẹn gặp lại sau 5 năm vào 2022!”, nhưng rồi lại gặp lại vào năm 2023
  • Đây là một khiếm khuyết của tiêu chuẩn POSIX, vốn đã mở rộng tiêu chuẩn C để cho phép sửa đổi biến môi trường
    • Phần gây bực nhất là nhiều người có thể tác động tới tiêu chuẩn hoặc duy trì thư viện C lại không xem đây là vấn đề
    • Lý do là vì đặc tả đã ghi rõ rằng không thể dùng setenv() cùng với luồng một cách an toàn
    • Vì vậy nếu ai đó làm vậy rồi bị crash thì đó là lỗi của họ
  • Thế nên chúng ta được bảo rằng phải “đọc kỹ đặc tả của mọi hàm, không dùng phần mềm do người khác viết, và không dùng luồng”
    • Nhưng đây là một giả định phi thực tế trong phần mềm hiện đại
    • Thay vào đó, nên cố gắng tạo ra các API khó bị dùng sai hơn và có thể tiến hóa theo thay đổi của hệ sinh thái
  • Vì ngôn ngữ C và thư viện chuẩn của nó vẫn tiếp tục giữ vai trò quan trọng ở nền tảng của phần lớn phần mềm, chúng ta cần tìm cách cải thiện nó hoặc tìm cách loại bỏ nó

Vì sao setenv() không thread-safe

  • getenv() trả về char*, và ứng dụng không cần giải phóng nó sau này
  • Trong lúc một luồng đang dùng con trỏ này, một luồng khác có thể thay đổi cùng biến môi trường đó bằng setenv() hoặc unsetenv()
  • Tiêu chuẩn C chỉ bao gồm getenv(), nhưng phần lớn các triển khai tuân theo POSIX và có thêm các hàm sửa đổi môi trường
  • putenv() thêm một char* vào tập biến môi trường, và nếu ứng dụng sửa bộ nhớ đó sau khi putenv() trả về thì biến môi trường cũng bị thay đổi
  • environ là một mảng con trỏ kết thúc bằng NULL (char**) mà ứng dụng có thể đọc và gán, và việc truy cập mảng này không an toàn với luồng

Cách triển khai biến môi trường

  • Khi ứng dụng ghi đè một biến hiện có, trình triển khai phải quyết định cách xử lý
  • glibc và Solaris/Illumos không bao giờ giải phóng biến môi trường, nên giá trị trả về từ getenv() là bất biến và có thể được dùng an toàn giữa các luồng
  • musl và FreeBSD/Apple thì giải phóng biến môi trường, nên có thể bị crash nếu dùng con trỏ trả về từ getenv() sau khi một luồng khác gọi setenv()
  • Bài toán thứ hai là bảo đảm tập biến môi trường được cập nhật theo cách an toàn với luồng, và đây là nguyên nhân gây crash trong glibc

Vì sao chương trình dùng biến môi trường

  • Biến môi trường hữu ích để cấu hình các thư viện dùng chung hoặc runtime ngôn ngữ được nhúng trong chương trình khác
  • Người dùng có thể thay đổi cấu hình mà không cần tác giả chương trình truyền cấu hình đó một cách tường minh
  • Nhiều thư viện gọi getenv(), và chương trình cần thay đổi các biến này để cấu hình những thư viện mà nó sử dụng

Cần giải quyết vấn đề này, và có thể làm như sau

  • Theo tôi, việc đây là một vấn đề đã được biết đến từ rất lâu là điều vô lý
  • Hàng nghìn giờ đã bị lãng phí để debug vấn đề này hoặc thảo luận cách né tránh nó
  • Các cách để giải quyết vấn đề
    • Tạo một triển khai an toàn với luồng như Illumos/Solaris
      • Cách này vẫn có giới hạn. setenv() sẽ làm rò rỉ bộ nhớ, và nếu chương trình dùng putenv() hoặc environ thì vẫn không an toàn
      • Nhưng dù vậy vẫn tốt hơn các triển khai hiện tại trên Linux và Apple
    • Cách thứ hai là thêm một API mới để lấy mọi biến môi trường sao cho an toàn với luồng ngay từ thiết kế, như getenv_s() của Microsoft
  • Giải pháp tôi thích là dùng cả hai hướng
    • Điều này giúp giảm khả năng phát sinh vấn đề cho các chương trình và thư viện hiện có, đồng thời cung cấp con đường để tránh hoàn toàn vấn đề cho mã mới hoặc các ngôn ngữ như Go và Rust
    • Tương tự getenv_s(), thêm một hàm sao chép một biến môi trường vào bộ đệm do người dùng chỉ định
    • Thêm API an toàn với luồng để lặp qua tất cả biến môi trường hoặc sao chép toàn bộ chúng
    • Đánh dấu getenv() là không còn được khuyến nghị dùng nữa và thay vào đó khuyến nghị một hàm getenv() mới an toàn với luồng
    • Đánh dấu putenv() là không còn được khuyến nghị dùng nữa và thay vào đó khuyến nghị setenv()
    • Đánh dấu environ là đã lỗi thời và thay vào đó khuyến nghị dùng các hàm thao tác biến môi trường
    • Cập nhật phần triển khai biến môi trường để an toàn với luồng

3 bình luận

 
ahwjdekf 2023-11-24

"Vì trong đặc tả đã nêu rõ là không thể dùng setenv() cùng với thread" ==> khi sử dụng API hay SDK thì việc bắt buộc phải kiểm tra kỹ specification/đặc tả là điều cơ bản nhất trong những điều cơ bản. Nhìn kiểu này chẳng khác gì cố dùng cho bằng được.

 
carnoxen 2025-01-24

Vấn đề là ngay từ đầu đã dùng một tính năng có thiết kế sai.

 
cosine20 2023-11-27

....