Why We Are Moving Away from Xen and Hypervisors for Safety - Episode 2
저자: 코리 미냐드
신뢰성과 리눅스
이전 글에서는 안전이 중요한 소프트웨어의 신뢰성 문제의 심각성에 대해 이야기했습니다. 이번 글에서는 리눅스의 버그에 초점을 맞추겠습니다.
저는 MontaVista에서 커널 설계자로 일하고 있으며, 그동안 커널의 수많은 어려운 버그들을 해결해 왔습니다. 쉬운 버그들도 몇 개 해결했지만, 대부분은 다른 사람들이 처리합니다. 다른 사람들이 진전을 이루지 못한 후에야 제가 발견한 버그들입니다. 이러한 버그들을 통해, 현재 분석 기술을 사용하는 안전이 중요한 시스템에는 커널이 적합하지 않다고 생각하는 이유를 설명하고자 합니다.
기억을 짓밟는 사람
제가 먼저 이야기할 버그는 메모리 트래플러 버그입니다. 제가 커널에서 본 유일한 실제 메모리 트래플러 버그입니다. 제 경험과 관찰에 따르면 매우 드뭅니다. 정적 분석이나 검토를 통해 쉽게 찾을 수 있습니다. 보안 측면에서 크게 문제될 것은 없습니다.
이 상황에서는 페이지 테이블(struct page)의 일부가 무작위로 덮어쓰기되었습니다. 항상 페이지 테이블에 있었기 때문에 적어도 어느 정도 일관성은 유지했습니다. 커널을 수정하여 페이지 테이블이 읽기 전용이 되도록 했습니다. 그런 다음 페이지 테이블에 쓰는 모든 항목 주변에 코드를 추가하여 쓰기 중에 특정 페이지만 쓰기 가능하도록 설정했습니다. 그렇게 어려운 수정과 검토 및 테스트를 거친 후 패치를 보냈습니다. 고객은 이 패치를 적용하여 문제를 일으킨 페이지 테이블에 대한 쓰기를 포착했습니다. 문제는 실제로 고객이 작성한 커널 모듈에 있었습니다. 이 모듈은 이전 버전의 제품에서는 잘 작동했습니다. 근본 원인에 대한 정보는 없으며, 문제를 일으킨 함수를 지적한 후에도 고객은 더 이상 정보를 제공하지 않았습니다.
이런 종류의 버그가 전반적으로 저에게는 크게 문제가 되지는 않지만, 이 경험을 통해 커널은 커널에 대한 깊이 있는 이해가 없는 사람들에게는 적합하지 않다는 것을 깨달았습니다. 커널 엔지니어로서, 제가 자명하다고 생각하는 것들이 다른 사람들에게는 전혀 명확하지 않을 수 있습니다. 많은 고객이 자신의 필요에 맞춰 커널을 수정하는데, 이러한 수정에는 상당한 위험이 따릅니다.
펌웨어 버그
이상하게도 커널의 모든 버그가 커널에서 비롯된 것은 아닙니다. 한 고객이 연구실에 있는 카드에 리눅스를 설치했는데, 부팅 시나 부팅 직후에 카드가 다운되는 경우가 많았습니다. 그런데 그 고객이 커널 코어 덤프를 받아 저에게 보내주었습니다.
이 문제에 대한 분석은 정말 쉬웠습니다. 커널이 충돌했을 때 무슨 일이 일어났는지 살펴보고, 몇 가지를 살펴본 후 메모리에서 실행되던 머신 코드가 메모리에 있어야 할 코드와 일치하지 않는다는 것을 알게 되었습니다. 잘못된 메모리를 추출하여 고객에게 다시 보내면서, 고객에게 의미가 있기를 바랐습니다. 고객은 덤프에서 실험실 IP 주소를 인식했고, 저희는 그것이 ARP 패킷이라는 것을 알아냈습니다. 알고 보니 펌웨어가 커널을 시작하기 전에 이더넷 장치를 비활성화하지 않았던 것입니다. 커널이 이더넷 장치를 재설정하기 전에 패킷을 수신했다면, 메모리를 통해 DMA가 발생했을 것입니다.
경쟁 조건
현재 제가 개발한 프로젝트인 gensio의 테스트 스위트에서 발견된 버그를 수정하고 있습니다. 코드의 문제점을 파악하는 데 많은 시간을 투자했습니다. 커널을 탓하는 것은 컴파일러를 탓하는 것과 마찬가지입니다. 확실히 아는 게 좋습니다.
하지만 머리를 쥐어짜고 고민한 끝에 작은 복제 프로그램을 작성해 봤는데, 역시 커널 문제였습니다. 마스터 pty에 쓰고 pty를 닫으면 미묘한 경쟁 조건 때문에 데이터 중간에 데이터 덩어리가 가끔씩 드랍됩니다. 이 문제는 오랫동안 커널에 존재했지만 아무도 알아채지 못했습니다. tty 코드가 너무 복잡해서 관리자들이 어떻게 수정해야 할지 잘 모르겠습니다.
무료 버그 사용 후
마지막 예시로, 제가 최근 작업한 버그에 대해 이야기해 보겠습니다. 고객이 네트워크 이웃 코드, 즉 ARP 등을 처리하는 일반 코드를 심하게 손상시키고 있었습니다. 가끔씩 커널이 충돌했는데, 주로 타이머 코드나 그와 관련된 코드에서 발생했습니다. 타이머 데이터는 완전히 허위로 보였고, 몇 가지 분석과 디버깅 패치를 거친 후, 그 증거는 해제 후 사용(use-after-free)을 가리켰습니다. 타이머 데이터가 손상되어 타이머의 출처를 알 수 없었습니다. 당시에는 이웃 코드와 관련이 있다는 것을 몰랐습니다. 단지 타이머와 관련된 무언가가 충돌하고 있다는 것을 알고 있었습니다.
이를 추적하기 위해 데이터 구조에서 실행 중인 모든 타이머를 추적하는 코드를 작성한 다음, 알려진 실행 타이머가 해당 메모리 청크에 있는 경우 패닉을 발생시키는 메모리 해제 루틴에 코드를 추가했습니다.
그러다가 당연히 문제가 더 이상 발생하지 않았습니다. 하이젠버그(Heisenbug)였습니다. 무료 코드에 추가된 시간이 문제를 가릴 만큼 타이밍을 조정했을 것으로 추측합니다. 고객은 디버깅 코드를 그대로 두었고, 시스템 작동에 영향을 미치지 않을 만큼 효율적이었습니다. 몇 달 후, 마침내 문제가 발생했습니다. 이 문제는 이후 커널 패치에서 해결된 것으로 생각되지만 (아직 100% 확신할 수는 없습니다), 패치 헤더에는 이러한 유형의 경쟁에 대한 언급이 없었습니다.
그래서 뭐 ?
다음 게시물에서는 이 버그들이 설명적이라고 생각하는 이유에 대해 말씀드리겠습니다.