15P by xguru 2023-11-23 | favorite | 댓글 2개
  • C 언어의 setenv()unsetenv() 함수는 스레드를 사용하는 프로그램에서 안전하게 사용할 수 없음
  • 이 함수들은 전역 상태를 수정하며, 다른 스레드에서 getenv()를 호출할 때 충돌을 일으킬 수 있음
  • Go의 os.Setenv 및 Rust의 std::env::set_var()과 같이 C 표준 라이브러리 함수를 사용하는 다른 언어에서도 충돌 발생
  • Go 프로그램에서 관련 문제를 추적하고 버그를 보고하는 데 2일이 소요됨
    • Go의 DNS 리졸버 내부에서 getaddrinfo()를 사용하는데 이게 getenv()를 호출하기 때문
  • 근데 이 문제는 굉장히 오래 되었음. 2017년에도 관련 글이 있고, 글 하단에 5년뒤 2022년에 만나요! 라고 써있는데 2023년에 다시 만난 것
  • 이는 POSIX 표준의 결함으로, C 표준을 확장하여 환경 변수를 수정할 수 있도록 한 것
    • 가장 화가 나는 부분은, 표준에 영향을 미치거나 C 라이브러리를 유지 관리할 수 있는 많은 사람들이 이를 문제라고 생각하지 않는다는 것
    • 그 이유는 사양에 스레드와 함께 setenv()를 사용할 수 없다고 명확하게 명시되어 있기 때문
    • 따라서 누군가가 이 작업을 수행하면 충돌은 그들의 잘못임
  • 그러니 우리는 "모든 함수의 사양을 주의 깊게 읽고, 다른 사람이 작성한 소프트웨어를 사용하지 말고, 스레드를 사용하지 말아야 함"
    • 하지만 현대 소프트웨어에서 이것은 비현실적인 가정임
    • 이보다는 망치기 어렵고 생태계 변화에 따라 진화하는 API를 만들기 위해 노력해야 한다고 생각함
  • C 언어와 표준 라이브러리는 대부분의 소프트웨어의 기반에서 계속해서 중요한 역할을 하고 있으니, 이를 개선할 방법을 찾거나 아니면 버릴 방법을 찾아야 함

왜 setenv()는 Thread-Safe 하지 않은가

  • getenv()char*을 반환하며, 애플리케이션은 나중에 이를 해제할 필요가 없음
  • 한 스레드가 이 포인터를 사용하는 동안 다른 스레드가 setenv() 또는 unsetenv()를 사용하여 동일한 환경 변수를 변경할 수 있음
  • C 표준에는 getenv()만 포함되어 있으나, 대부분의 구현체는 POSIX 표준을 따르며 환경을 수정하는 함수를 포함함
  • putenv()는 환경 변수 집합에 char*를 추가하며, 애플리케이션이 putenv() 반환 후 메모리를 수정하면 환경 변수도 수정됨
  • environ은 애플리케이션이 읽고 할당할 수 있는 NULL로 종료되는 포인터 배열(char**)이며, 이 배열에 대한 접근은 스레드 안전하지 않음

환경 변수 구현 방식

  • 애플리케이션이 기존 변수를 덮어쓸 때 구현체는 어떻게 처리할지 결정해야 함
  • glibc와 Solaris/Illumos는 환경 변수를 절대 해제하지 않으며, getenv()에서 반환된 값은 불변이고 스레드에 안전하게 사용될 수 있음
  • musl과 FreeBSD/Apple은 환경 변수를 해제하며, 다른 스레드가 setenv()를 호출한 후 getenv()에서 반환된 포인터를 사용하면 충돌이 발생할 수 있음
  • 환경 변수 집합이 스레드 안전하게 업데이트되는 것을 보장하는 것이 두 번째 문제이며, 이로 인해 glibc에서 충돌이 발생함

프로그램이 환경 변수를 사용하는 이유

  • 환경 변수는 다른 프로그램에 포함된 공유 라이브러리나 언어 런타임을 구성하는 데 유용함
  • 사용자가 프로그램 작성자가 명시적으로 구성을 전달할 필요 없이 구성을 변경할 수 있음
  • 많은 라이브러리가 getenv()를 호출하며, 프로그램은 사용하는 라이브러리를 구성하기 위해 이 변수들을 변경해야 함

이 문제를 해결해야 하며, 다음과 같이 할 수 있음

  • 내 생각에 이것이 오랫동안 알려진 문제였다는 것은 말도 안 되는 일임
  • 문제를 디버깅하거나 해결 방법을 논의하느라 수천 시간을 낭비함
  • 문제를 해결할 방법
    • Illumos/Solaris와 같이 스레드에 안전한 구현을 만들기
      • 이것은 약간 한계가 있음. setenv()에서 메모리가 누수되고 프로그램이 putenv() 또는 환경 변수를 사용하는 경우 여전히 안전하지 않음
      • 하지만 이는 현재 Linux 및 Apple 구현에 비해 개선된 것
    • 두 번째로는 Microsoft의 getenv_s()와 같이 설계상 스레드에 안전한 모든 환경 변수를 가져오는 새로운 API를 추가하는 것
  • 내가 선호하는 해결책은 두 가지를 모두 사용하는 것
    • 기존 프로그램과 라이브러리에 대한 문제 발생 가능성을 줄이고, Go와 Rust와 같은 새로운 코드나 언어에 대한 문제를 완전히 피할 수 있는 경로를 제공함
    • getenv_s()와 유사하게 하나의 환경 변수를 사용자가 지정한 버퍼에 복사하는 함수를 추가
    • 모든 환경 변수를 반복하거나 모든 변수를 복사할 수 있는 스레드 안전 API를 추가
    • getenv()를 더 이상 사용되지 않는 것으로 표시하고 대신 스레드에 안전한 새로운 getenv() 함수를 권장
    • putenv()를 더 이상 사용되지 않는 것으로 표시하고 대신 setenv()를 권장
    • environ을 사용 중단된 것으로 표시하고 대신 환경 변수 함수를 권장
    • 스레드에 안전하도록 환경 변수 구현을 업데이트

"사양에 스레드와 함께 setenv()를 사용할 수 없다고 명확하게 명시되어 있기 때문" ==> api 나 sdk 를 사용 시에는 반드시 specification 명세를 꼼꼼히 확인해야 함이 기본 중의 기본입니다. 어거지 쓰는걸로밖에 안보이네요.