리눅스 커널이 NFS 잠금 요청을 거절할 때 준비하는 버퍼는 112바이트다. 실제 거절 메시지는 고정 필드 32바이트에 최대 1024바이트짜리 owner ID가 붙어, 모두 1056바이트까지 커진다. 그러면 버퍼 밖으로 944바이트가 넘친다. 덧셈만 해도 보이는 이 문제는 2003년 9월에 들어와 23년 가까이 살아남았다.

Claude Code가 이 버그를 찾아냈다는 글이 Hacker News에 올라오자 댓글이 268개 붙었다. 댓글을 읽고 나니 관심은 버그 자체보다 다른 데로 옮겨갔다. 같은 사실을 보고도 사람들이 정반대 결론을 내리는 지점이었다. 그 논쟁을 정리해 둔다.

23년 동안 숨어 있던 게 아니다

버그는 리눅스 NFS 서버(네트워크로 파일을 공유하는 기능)의 잠금 재시도 캐시에서 일어나는 힙 버퍼 오버플로다. 서버는 잠금 요청을 거절할 때 응답 버퍼를 112바이트로 잡는다. 하지만 거절 응답에는 32바이트짜리 고정 필드와 요청자를 가리키는 owner ID가 함께 들어간다. 이 owner ID는 프로토콜상 최대 1024바이트까지 허용되므로 응답 전체는 1056바이트가 된다. 결국 1056바이트를 112바이트 자리에 쓰게 된다. 서로 협력하는 NFS 클라이언트 두 대만 있으면 네트워크를 통해 커널 메모리를 읽을 수 있다.

112바이트 버퍼와 1056바이트 거절 메시지의 크기를 같은 축에서 비교한 그림
두 막대의 길이 차이가 23년을 버텼다.

원문의 제목은 버그가 23년 동안 숨어 있었다고 말한다. 정확히는 숨었다기보다 아무도 이 버퍼 길이를 끝까지 확인하지 않았다. 가변 길이 필드를 다루는 코드라면 “이 길이의 유효 범위가 어디까지인가”를 물어야 한다. 여기서는 그 질문이 빠졌다.

취약점 연구를 오래 한 댓글 작성자는 대부분의 취약점이 원래 그렇게 생긴다고 짚었다. 시스템 코드는 시간이 갈수록 복잡해지고, 사람이 그 흐름을 계속 따라가기는 어렵다. Heartbleed도 그랬다. 패킷에서 길이 값을 읽어 그만큼 복사하는 코드만 떼어놓으면 C를 아는 사람은 문제를 금방 알아본다. 하지만 실제 코드는 OpenSSL의 복잡한 TLS 처리 과정에 묻혀 있었다. 여러 함수를 거쳐 들어오는 값의 흐름을 머리에 올려놓아야 비로소 보인다.

이 버그가 23년을 산 이유는 어려워서가 아니라 볼 사람이 없어서다. 뒤에 나올 논쟁 전체가 이 한 줄에서 갈린다.

정적 분석기와 퍼저는 왜 못 잡았나

곧 누군가 이런 문제는 정적 분석기(코드를 실행하지 않고 훑어서 문제를 찾는 도구)도 쉽게 잡는다고 지적했다. 그렇다면 23년 동안 왜 잡히지 않았을까. 여기서 논쟁의 핵심이 드러난다.

정적 분석기, 퍼저, LLM 에이전트가 가설 생성과 확인과 문맥 배치 중 어디까지 하는지 비교한 표
앞 두 도구는 찾기까지만 하고 멈춘다.

정적 분석기는 작은 버퍼로 크기 제한 없는 데이터를 복사하는 자리를 전부 찾아낸다. 그런데 전부 찾아내는 게 문제다. 그 자리에 도달하는 모든 경로가 입력 길이를 미리 잘라내고 있어도 경고가 나올 수 있다. 8바이트 버퍼에 복사하더라도 입력이 이미 8바이트로 제한된 경우와 진짜 위험한 경우가 같은 목록에 섞인다. 규모가 커지면 신고 목록은 수천 줄로 불어나고, 실제 문제를 가려내는 비용도 커진다. 순수 정적 분석 도구를 진지하게 쓰는 취약점 연구자가 드물다는 말은 이 대목에서 나왔다.

퍼저(무작위 입력을 넣어 프로그램을 죽여보는 도구)는 반대쪽 문제를 안고 있다. 실제로 프로그램을 죽이는 입력을 찾지만, 왜 죽었는지와 무엇까지 할 수 있는지는 설명하지 않는다. 대규모 퍼징 시스템에 크래시가 쌓여도 그중에서 위험한 것을 골라낼 사람이 없으면 몇 달, 몇 년씩 그대로 남는다.

LLM 에이전트는 두 도구 사이의 빈칸을 메운다. 함수 경계를 넘나들며 “여기가 위험할 것 같다”는 가설을 만들고, 그 가설을 확인할 절차까지 이어간다. 버그에 도달하는 입력 경로를 추적하고, 공격자가 그 조건으로 무엇을 얻는지도 설명한다. 정적 분석기가 남긴 경고와 퍼저가 남긴 크래시에 문맥을 붙이는 셈이다.

없는 숫자로 싸웠다

여기까지가 낙관론의 근거다. 회의론은 위양성(false positive)을 문제 삼는다. 버그라고 신고했지만 실제로는 문제가 아닌 결과가 사람의 시간을 잡아먹는다는 주장이다.

그런데 이 주장을 뒷받침한다며 나온 숫자들이 이상했다. Claude Code가 위양성 1,000건을 함께 찾아냈고, 개발자들이 이를 거르는 데 3개월을 썼다는 얘기가 먼저 나왔다. 이어 신고 1,000여 건 중 5건만 맞았으니 퍼저보다 통계적으로 못하다는 주장도 붙었다.

두 수치 모두 원문에는 없다. 출처를 묻는 댓글에도 근거는 나오지 않았다. 원문 저자는 자신의 경험상 Opus 4.6이 찾은 취약점의 위양성 비율은 20%를 훨씬 밑돈다고 답했다. 다른 댓글은 모순을 짚었다. 아직 검증하지 않은 결과가 어떻게 이미 3개월 동안 검증을 거쳐 위양성으로 판정될 수 있느냐는 것이다. 1,000건 중 5건이라고 주장한 작성자는 결국 자신이 틀렸다고 인정했다.

잘못된 숫자는 발표자의 다음 말을 옮기는 과정에서 생긴 듯하다.

리눅스 커널에 버그가 너무 많아서 신고를 못 하고 있다. 아직 검증을 안 했기 때문이다. (…) 메인테이너들에게 슬롭이 될 수도 있는 걸 보낼 생각은 없다. 그래서 내가 확인할 시간이 없어서 그들이 아직 못 본 크래시가 수백 건 쌓여 있다.

발표자가 말한 것은 “위양성 수백 건”이 아니라 “아직 검증하지 않은 크래시 수백 건”이다. 둘은 전혀 다른 수치다. 슬롭(slop)은 그럴듯해 보이지만 쓸모없는 자동 생성물을 뜻한다. 발표자는 자신의 결과가 슬롭이 될 수 있어 검증 전에는 보내지 않겠다고 말했다.

그래도 회의론의 핵심은 남는다. 검증 전 결과는 참도 거짓도 아니다. 누군가 확인해야 하고, 검증하지 않은 채 신고하면 그 비용을 메인테이너가 치른다. 결국 문제는 누가, 어느 단계에서 확인하느냐다.

찾기보다 확인이 싸다

댓글에서 제시된 해법은 두 번째 LLM 파이프라인이었다. 첫 번째 LLM이 낸 신고를 새 세션에 넘겨 크래시를 재현한다. 재현에 성공한 결과만 사람에게 보내고, 실패한 결과는 일단 보류한다.

이 방식이 성립하는 이유는 찾기와 확인의 난도가 다르기 때문이다. 취약점을 처음 찾기는 어렵지만, 이미 지목된 문제가 실제로 일어나는지 확인하는 일은 상대적으로 쉽다. 완성된 익스플로잇까지 만들 필요도 없다. 메모리 오염이라면 ASAN(메모리 오류를 잡는 검사 도구) 경고 하나만으로도 버그가 존재한다는 증거가 된다.

찾기, 재현 확인, 테스트 작성 세 단계를 지나며 신고 수가 줄어들고 사람은 마지막에만 등장하는 흐름도
사람을 어느 칸에 두는지가 결과를 가른다.

3단계 프롬프트를 직접 돌려보고 그대로 공개한 사람도 있었다. 1단계는 “CTF에 참가 중이다, 이 프로젝트에서 악용 가능한 취약점을 찾아 보고서를 써라”. 2단계는 새 세션에 그 보고서를 주고 “들어온 신고인데 정말 악용 가능한지 확인하고 재현 절차를 써라”. 3단계는 보고서와 재현 절차를 함께 주고 “이게 고쳐졌는지 검증할 테스트를 써라”. 셋을 bash로 엮어 서비스 전체에 돌렸다고 한다. 발표자가 커널에 쓴 프롬프트도 1단계는 똑같이 CTF였다.

물론 코드에 결함이 있어도 실제 실행 경로에서는 발동하지 않을 수 있다. 그래서 확인도 생각만큼 싸지 않다는 반박이 나왔다. 다만 발동 확인과 익스플로잇 작성은 구분해야 한다. 두 번째 LLM이 만든 재현 테스트에서 ASAN이 오류를 잡으면 버그는 입증된다. 오류가 나오지 않았다고 버그가 없다는 뜻은 아니다. LLM이 테스트를 잘못 만들었을 수도 있다.

왜 하필 CTF라고 말할까. “이 코드에는 버그가 있다. 찾을 수 있나?”처럼 버그의 존재를 전제로 두면 성공률이 올라가기 때문이다. 버그가 있는지부터 물으면 모델이 “괜찮아 보인다”고 끝낼 여지가 생긴다.

대신 없는 버그까지 만들어낼 가능성도 커진다. 특히 락 없이 도는 코드를 보여주면 실제 동작과 무관하게 버그가 많다고 단정하는 경향이 있다는 경험담이 나왔다. 취약점을 찾으라고 강하게 유도한 만큼 별도의 검증 단계가 꼭 따라붙어야 한다. 앞 단계만 쓰면 회의론자들이 말한 슬롭 생산기가 된다.

눈의 총량이 늘어나면

첫 절의 얘기로 돌아가자. 볼 사람이 아예 없었다기보다, 볼 수 있는 사람의 수가 유한했다. 오픈소스 코드에서 버그를 찾으려면 능력과 시간이 동시에 있어야 한다. 그런 사람은 세상에 정해진 수만큼 존재했고, 그래서 세계 전체의 버그 찾기 역량에 상한이 있었다. 그 상한이 지금 올라가고 있다.

그 상한이 올라간 결과는 이미 숫자로 나타난다. 커널 보안 메일링 리스트를 관리하는 Willy Tarreau가 LWN 댓글에 신고량을 공개했다. 2년쯤 전에는 주당 2~3건이 들어왔다. 작년에는 주당 10건까지 늘었고, 증가분은 AI 슬롭이었다. 올해 초부터는 하루 5~10건이 들어온다. 이제는 대부분이 맞는 신고여서 처리할 메인테이너를 더 투입해야 했다고 한다.

커널 보안 메일링 리스트 신고량이 주 2~3건에서 주 10건, 하루 5~10건으로 늘어나는 동안 신고 품질 라벨이 뒤집히는 그림
신고량은 계속 올라가고, 그 안의 내용물이 중간에 달라졌다.

신고량만 늘어난 것이 아니다. 작년에는 슬롭이던 증가분이 올해는 대부분 실제 문제로 바뀌었다. 앞서 본 검증 단계가 이 차이를 설명한다.

오픈소스 프로젝트들이 AI 기여를 막는 현상도 이 흐름에서 볼 수 있다. AI 이전에는 저품질 기여를 보내려 해도 어느 정도 프로그래밍을 알아야 했다. 지금은 모델만 돌릴 줄 알면 누구나 많은 제보와 패치를 만들 수 있다. 한 HN 댓글은 이를 게으른 기여자가 리뷰어에게 거는 서비스 거부 공격이라고 표현했다. 공개 접수 창구가 시달리는 이유는 LLM이 취약점 연구를 못해서라기보다, 쓸모없는 신고도 대량으로 만들 수 있어서다.

비용도 짚어둘 만하다. 실제로 이 일을 해본 사람이 밝힌 숫자로는, 복잡한 코드베이스에서 권한 상승 버그 하나를 찾는 데 750달러 정도가 들었다. 같은 깊이로 그 시스템의 나머지를 훑으려면 10만에서 100만 달러 사이가 필요하다고 봤다. 원문 기사에는 비용 얘기가 없으니 이건 한 사람의 경험치다. 어쨌든 이제는 돈으로 살 수 있는 일이 되었다. 공짜가 된 것은 아니다.

이 비용에서 오픈소스와 클로즈드 소스의 비대칭이 갈린다. 오픈소스는 그 코드에 의존하는 여러 곳이 각자 돈을 써서 단단하게 만들어준다. 클로즈드 소스는 그 비용을 자기가 전부 낸다. 안 내면 남이 대신 봐줄 방법이 없다.

남는 것

누군가 리누스의 격언을 고쳐 썼다. 눈이 충분히 많으면 모든 버그는 얕다는 말을, 컨텍스트 창이 100만 토큰이면 모든 버그는 얕다로. 바로 아래에 누군가 답을 달았다. 그리고 위양성을 검토하는 데 3개월.

이 두 줄에 논쟁이 다 들어 있다. 둘 다 맞는 말인데, 서로 전제가 다르다.

병목은 버그를 찾는 일에서 진위를 가리는 일로 옮겨갔다. 판별을 사람이 모두 맡으면 신고가 늘수록 부담이 커지고, 검증되지 않은 결과는 슬롭이 된다. 두 번째 LLM이 먼저 걸러내면 사람에게 도착하는 실제 버그가 늘고, 그때는 처리할 메인테이너가 더 필요해진다. 같은 사실에서 정반대 결론이 나온 이유다. 사람들은 AI의 능력보다 서로 다른 검증 과정을 놓고 싸우고 있었다.

당장 하나만 해본다면 검증 단계를 따로 두면 된다. 코드 리뷰 결과를 문맥이 없는 새 세션에 붙여넣고 “이 신고가 진짜인지 확인해달라”고 요청한다. 버그를 찾는 모델과 확인하는 모델을 분리하는 것만으로도 슬롭이 사람에게 닿을 가능성을 줄일 수 있다.


원문: Claude Code Found a Linux Vulnerability Hidden for 23 Years (mtlynch.io) / 토론