저자: 코리 미니야드
신뢰성과 리눅스
이전 글에서 저는 안전에 중요한 소프트웨어의 신뢰성 문제가 얼마나 심각한지 이야기했습니다. 이번 글에서는 리눅스에서 발생하는 버그에 대해 살펴보겠습니다.
저는 MontaVista의 커널 아키텍트로 일하고 있으며, 그동안 커널에서 수많은 까다로운 버그들을 해결해 왔습니다. 물론 쉬운 버그들도 있었지만, 그런 것들은 대부분 다른 사람들이 처리했습니다. 제가 맡게 된 버그들은 다른 사람들이 진전을 이루지 못한 것들입니다. 저는 이러한 버그들 중 일부를 예시로 들어 현재의 분석 기술로는 커널이 안전에 중요한 시스템에 적합하지 않다고 생각하는 이유를 설명하고자 합니다.
기억을 짓밟는 자
제가 먼저 이야기할 버그는 메모리 트램플러입니다. 제가 커널에서 본 유일한 실제 메모리 트램플러이며, 제 경험과 관찰에 따르면 매우 드뭅니다. 정적 분석이나 코드 리뷰를 통해 쉽게 찾을 수 있습니다. 저는 보안 측면에서 이러한 버그가 크게 문제된다고 생각하지 않습니다.
이 상황에서 페이지 테이블(struct page)의 일부가 무작위로 덮어쓰기되는 문제가 발생했습니다. 다행히 해당 부분은 항상 페이지 테이블에 존재했기 때문에 어느 정도 일관성은 유지되었습니다. 저는 커널을 수정하여 페이지 테이블을 읽기 전용으로 만들고, 페이지 테이블에 쓰는 모든 코드 주변에 쓰기 작업이 진행되는 동안에만 해당 페이지가 쓰기 가능 상태로 설정되도록 했습니다. 이러한 다소 복잡한 수정 작업과 검토 및 테스트를 거쳐 패치를 고객에게 보냈습니다. 고객은 이 패치를 적용하여 문제를 일으킨 페이지 테이블 쓰기 부분을 찾아냈습니다. 알고 보니 문제는 고객이 작성한 커널 모듈에 있었습니다. 이 모듈은 이전 버전의 제품에서는 정상적으로 작동했었습니다. 고객은 문제의 원인이 되는 함수를 지적한 후 더 이상의 정보를 제공하지 않아 근본적인 원인에 대한 정보는 얻지 못했습니다.
이런 종류의 버그는 전반적으로 저에게 큰 문제가 되지는 않지만, 이번 경험을 통해 커널은 커널에 대한 깊은 이해가 없는 사람들이 손댈 곳이 아니라는 것을 알게 되었습니다. 커널 엔지니어인 저에게는 당연하게 여겨지는 것들이 다른 사람들에게는 전혀 당연하지 않을 수 있습니다. 많은 고객들이 자신의 필요에 맞게 커널을 수정하는데, 이러한 수정에는 상당한 위험이 따릅니다.
펌웨어 버그
놀랍게도 커널의 모든 버그가 커널 자체에서 발생하는 것은 아닙니다. 한 고객이 연구실에서 리눅스를 탑재한 카드를 사용하고 있었는데, 부팅 시 또는 직후에 카드가 자주 다운되는 문제가 발생했습니다. 고객은 커널 코어 덤프를 추출하여 저에게 보내주었습니다.
이 문제에 대한 분석은 사실 꽤 쉬웠습니다. 커널이 충돌했을 때 무슨 일이 일어났는지 살펴보고, 이것저것 확인해 본 결과 메모리에서 실행되고 있던 기계어 코드가 메모리에 있어야 할 코드와 일치하지 않는다는 것을 알게 되었습니다. 잘못된 메모리 부분을 추출해서 고객에게 보내드렸는데, 혹시 도움이 될까 싶었습니다. 고객은 덤프에서 연구실 IP 주소를 알아봤고, 그것이 ARP 패킷이라는 것을 알아냈습니다. 알고 보니 펌웨어가 커널을 시작하기 전에 이더넷 장치를 비활성화하지 않았던 것입니다. 커널이 이더넷 장치를 재설정하기 전에 패킷이 수신되면 메모리를 통해 DMA 전송이 발생했던 것입니다.
레이스 조건
저는 현재 제가 개발한 프로젝트인 gensio의 테스트 스위트에서 발견된 버그를 수정하고 있습니다. 제 코드에 무엇이 잘못되었는지 파악하는 데 많은 시간을 쏟았습니다. 결국 커널 탓을 하는 건 컴파일러 탓을 하는 것과 마찬가지니까요. 정말 확실하게 확인해야 합니다.
하지만 머리를 싸매고 고민한 끝에 간단한 재현 코드를 작성해 보니, 아니나 다를까 커널이 문제였습니다. 마스터 pty에 데이터를 쓰고 pty를 닫으면 미묘한 경쟁 조건 때문에 데이터 중간 부분이 간혹 손실되는 현상이 발생합니다. 이 문제는 오랫동안 커널에 존재해 왔지만 아무도 알아채지 못했습니다. tty 코드가 너무 복잡해서 관리자들조차 어떻게 수정해야 할지 확신하지 못하고 있습니다.
무료 사용 후 버그
마지막으로 제가 최근에 작업했던 버그 사례를 말씀드리겠습니다. 고객사에서 네트워크 인접 노드 코드, 즉 ARP 등을 처리하는 일반 코드에 상당한 부하를 주었습니다. 커널이 간헐적으로 충돌했는데, 주로 타이머 코드 또는 그와 관련된 부분에서 발생했습니다. 타이머 데이터가 완전히 잘못된 것으로 보였고, 분석 및 디버깅 패치를 통해 use after free 오류가 원인임을 알게 되었습니다. 타이머 데이터가 손상되어 타이머가 어디에서 왔는지 알 수 없었습니다. 당시에는 이 문제가 인접 노드 코드와 관련이 있다는 것을 알지 못했고, 단지 타이머와 관련된 부분에서 충돌이 발생한다는 사실만 알고 있었습니다.
이 문제를 추적하기 위해 실행 중인 모든 타이머를 데이터 구조에 저장하는 코드를 작성한 다음, 메모리 해제 루틴에 알려진 실행 중인 타이머가 해당 메모리 영역에 있으면 오류를 발생시키는 코드를 추가했습니다.
그러다가 갑자기 문제가 사라졌습니다. 하이젠버그였죠. 아마도 자유 코드에 추가된 시간 때문에 타이밍이 충분히 어긋나서 문제가 가려진 것 같습니다. 고객은 디버그 코드를 그대로 두었는데, 시스템 작동에 영향을 줄 만큼 효율적이었기 때문입니다. 몇 달 후, 마침내 문제가 발생했습니다. 이 문제는 나중에 나온 커널 패치에서 수정된 것으로 추정되지만(최근 일이라 100% 확신할 수는 없습니다), 패치 헤더에는 이런 유형의 경쟁 조건에 대한 언급이 없었습니다.
그래서 뭐 ?
다음 글에서는 왜 이 벌레들이 교훈적인 사례라고 생각하는지 이야기해 보겠습니다.
Please reach out to discuss your particular scenario today.