RocksDB에 추가된 단위 테스트가 새로운 CPU의 하드웨어 버그를 드러낸 과정을 소개합니다.
이것은 내가 4년 전에 추가한 RocksDB 단위 테스트 하나, 작은 스트레스 테스트라고 부를 수도 있는 것이 어떻게 더 새로운 CPU의 새로운 하드웨어 버그를 드러냈는지에 대한 이야기입니다. 이 문제는 “높은 심각도” CVE가 할당될 만큼 충분히 심각했습니다.
약 4년 전, 우리는 캐싱 목적을 위해 서로 다른 파일시스템 전반에서 안정적인 식별자를 부여하기 위해 SST 파일에 고유 식별자를 추가했습니다. 여기서 동기의 일부는 OS 파일시스템이 제공하는 파일 고유 식별자의 유일성과 재사용되지 않음에 대한 의존성을 제거하는 것이었습니다. (일부 파일시스템은 최근 이력까지 포함한 모든 파일 사이의 유일성이 아니라, 현재 존재하는 파일들 사이에서만 유일성을 보장하고 있었습니다.) 나는 이 의존성 문제를 기존 해결책을 재사용하는 것과 코드 자체 의존성 사이의 큰 긴장 이라고 부르고 싶습니다. 다른 사람의 작업을 중복하고 싶지는 않지만, 그들의 버그나 변하는 / 어긋난 요구사항의 영향을 받고 싶지도 않습니다. 이 균형을 맞추는 일은 까다로울 수 있지만, 이 경우에는 가능한 모든 파일시스템이 품질 좋은 고유 식별자를 제공한다고 믿고 싶지 않다는 점이 우리에게 분명했습니다.
큰 난수들(예: 128비트)에 익숙하다면, 각 파일과 함께 난수 식별자(또는 준난수, 이것은 내가 논문에서 형식화하는 데 도움을 주었습니다, arXiv에도 있습니다)를 영속적으로 저장하는 것이 OS 파일시스템의 사소한 기능에 그렇게 결정적으로 의존하는 것보다 더 안전하고 예측 가능하다는 데 아마 동의할 것입니다.
하지만 그것은 우리가 고품질 난수에 접근할 수 있다고 가정합니다(적어도 시작점으로 쓸 좋은 것 한두 개는 있어야 합니다 - 논문을 참고하세요). RocksDB는 크로스플랫폼을 지향하기 때문에, 우리는 플랫폼별 의존성을 최소화하고 크로스플랫폼 의존성을 선호합니다. 하지만 그렇게 하면 우리가 원하지 않았던 지점으로 쉽게 되돌아갈 수 있습니다. 즉, 우리가 필요로 하는 것의 어느 한 구현에서 발생한 버그나 문제에 취약해지는 것입니다.
다행히 난수 엔트로피의 특성상 여러 소스를 결합 할 수 있어서, 결과의 품질은 최고의 입력 소스만큼 좋아집니다. 따라서 하나가 나쁘더라도 모두가 나쁜 경우에만 문제가 됩니다. 그리고 우리에게는 다음과 같은 장점이 있었습니다. (a) 우리는 보안이 아니라 유일성만 필요했기 때문에 추가적인 면밀한 검토의 필요성이 줄어들었고 준난수 접근법을 사용할 수 있었으며, (b) 준난수 접근법은 필요한 엔트로피 양을 최소화했기 때문에 각 엔트로피 단위를 얻는 성능 비용이 거의 무시할 만했습니다. 그래서 나는 다음 엔트로피 소스들을 결합했습니다:
이들 각 소스의 품질을 지속적으로 검증하기 위해, 나는 단위 테스트를 추가했습니다. 이 테스트는 한 번에 위 소스들 중 하나만을 기반으로 수천 개의 고유 식별자를 많은 스레드로 생성하고, 그 유일성을 검증했습니다. 고품질 소스의 경우, 수천 개 사이에서 중복된 128비트 ID가 하나라도 생길 확률은 무시할 만큼 작으며, 이런 테스트를 수십 년 동안 계속 실행하더라도 마찬가지입니다.
몇 달 전까지는 거의 그게 전부인 이야기였습니다. 그러다 std::random_device 기반 테스트가 한 번 실패했습니다. 꽤 수상했던 이유는 고유 ID 수가 기대치보다 단지 하나 부족한 정도가 아니라, 수십 개 혹은 수백 개 부족했기 때문입니다. 하지만 그조차도 처음부터 생성된 ID 수가 더 적었던 어떤 무작위 CPU 문제나 비트 뒤집힘으로 설명할 수는 있었습니다. (RocksDB 개발 노력과 CPU 시간의 점점 더 많은 부분이 논리적으로는 중복되지만 손상이 너무 멀리 전파되기 전에 CPU 오계산을 감지하기 위해 존재하는 검사들에 들어가고 있다는 점을 눈치챘을지도 모르겠습니다.)
그런데 약 한 달 뒤 또 실패했습니다. 4년 동안 실패가 없다가, 두 달 안에 두 번 실패한 것입니다. 이건 정말 좋지 않은 냄새가 났습니다. 세부 사항을 파고들다가 나는 결정적인 상관관계를 발견했습니다. 실패한 두 테스트 작업이 완전히 다른 데이터 센터에서 실행되었음에도 같은 유형의 하드웨어에서 돌았던 것입니다.
거기서부터 나는 엔지니어라면 자연스럽게 할 일을 했습니다. 실패를 재현하려고 규모를 키운 것입니다. 그리고 그것은 놀라울 만큼 쉬웠습니다. 작업의 스레드 수를 코어 수 정도까지 늘리자 같은 종류의 새로운 CPU를 사용하는 모든 시스템에서 빠르고 일관되게 실패했고, 다른 모든 것에서는 통과했습니다. 나는 다음을 포함해 몇 가지 변형도 시험해 더 많은 세부 사항을 확인했습니다.
std::random_device는 영향받지 않았고,그 다음부터 Meta 동료들이 저수준 세부 사항을 조사했습니다. 그들은 이 문제의 원인이 이 유형의 프로세서에서 RDSEED 명령어가 무작위적으로 기대되는 것보다 훨씬 더 자주 0과 “success”를 반환한다는 점임을 밝혀냈습니다. 하지만 이것은 일부 코어에서만, 그리고 한 동료의 표현을 빌리면 “메모리 부하 하에서 재현 가능한 복잡한 마이크로아키텍처 조건”에서만 발생했습니다. 이러한 프로세서에서 RDSEED를 사용할 수 없다는 신호를 주기 위한 완화용 Linux 커널 패치가 개발되었고, OEM에서 수정 사항이 나올 때까지 문제를 피하기 위해 Meta 내부에 이를 배포할 의도였습니다. AMD는 빠르게 이 문제를 인정하고 계획된 완화를 발표했습니다. 여기에는 CPU 마이크로코드 업데이트가 포함되었습니다.
OEM이 이 문제를 공개적으로 인정할 때까지 정보를 기밀로 유지하려고 노력했지만, Linux 메일링 리스트를 통한 비조율 공개는 Meta 내 여러 인프라 팀에 걸친 열성적인 대응 노력 때문에 발생했습니다. 우리는 이 실수를 유감스럽게 생각하며, 먼저 OEM과 조율하지 못했던 프로세스의 통제를 개선하기 위해 노력하고 있습니다.