리눅스 커널이 NFS 잠금 요청을 거절할 때 쓰는 버퍼는 112바이트다. 그런데 거절 메시지에는 최대 1024바이트짜리 필드가 붙는다. 합치면 1056바이트를 112바이트짜리 통에 밀어 넣는 셈이라, 세어보면 바로 보인다. 이 코드는 2003년 3월에 들어와 23년을 살아남았다.
Claude Code가 이걸 찾아냈다는 글이 Hacker News에 올라왔고 댓글이 268개 붙었다. 읽고 나서 남은 건 버그도, AI가 버그를 잘 찾느냐도 아니었다. 같은 사실을 놓고 정반대 결론이 나오는 자리가 하나 있었다. 그 자리를 정리해 둔다.
23년 동안 숨어 있던 게 아니다
버그의 정체부터. 리눅스 NFS 드라이버(네트워크로 파일을 공유하는 기능)의 잠금 재시도 캐시에서 일어나는 힙 버퍼 오버플로다. 서버가 잠금 요청을 거절할 때 응답을 담을 버퍼를 112바이트로 잡는다. 거절 응답에는 요청자를 가리키는 owner ID가 들어가는데, 이 값이 프로토콜상 최대 1024바이트까지 허용된다. 그래서 1056바이트가 112바이트 자리에 쓰인다. 협력하는 NFS 클라이언트 두 대만 있으면 네트워크를 통해 커널 메모리를 읽을 수 있다.
제목이 조금 틀렸다. 아무도 이 버퍼 길이를 확인해 볼 생각을 안 했을 뿐이다. 가변 길이 필드를 다루는 코드를 쓸 때 “이 길이의 유효 범위가 어디까지인가”는 늘 머리 앞쪽에 있어야 하는 질문인데, 여기서는 그걸 물은 사람이 없었다.
취약점 연구를 오래 한 사람이 더 정확하게 짚었다. 대부분의 취약점이 원래 그렇게 생긴다. 시스템 코드는 복잡도가 이자처럼 쌓이고, 사람 손으로 그걸 따라가는 건 대단히 어렵다. Heartbleed도 같은 얘기였다. 패킷에서 길이 값을 읽어 그만큼 복사한다는 코드는 떼어놓고 보면 C를 아는 사람 누구나 즉시 알아본다. 그런데 그 코드는 OpenSSL의 TLS 처리 더미 아래 묻혀 있었다. 함수에 들어오는 입력이 무엇인지를 머리에 전부 올려두고 따라가야 보인다.
이 버그가 23년을 산 이유는 어려워서가 아니라 볼 사람이 없어서다. 뒤에 나올 논쟁 전체가 이 한 줄에서 갈린다.
정적 분석기와 퍼저는 왜 못 잡았나
이런 건 정적 분석기(코드를 실행하지 않고 훑어서 문제를 찾는 도구)가 쉽게 잡는다고 곧 누군가 지적했다. 맞는 말이라서 오히려 질문이 남는다. 그러면 23년 동안 왜 안 잡혔나. 답을 따라가면 논쟁에서 가장 알맹이 있는 대목이 나온다.
정적 분석기는 작은 버퍼로 크기 제한 없는 데이터를 복사하는 자리를 전부 찾아낸다. 그런데 전부 찾아내는 게 문제다. 그 자리에 도달하는 모든 경로가 입력 길이를 미리 잘라내고 있어도 똑같이 신고한다. 8바이트 버퍼에 복사를 하는데 입력이 이미 8바이트로 제한된 경우와 진짜 위험한 경우를 구분해주지 않는다. 그래서 규모가 커지면 신고 목록이 수천 줄이 된다. 어느 것이 실제 문제인지 가려내는 비용을 감당할 수 없다. 그래서 순수 정적 분석 도구를 진지하게 쓰는 취약점 연구자는 거의 없다는 말이 나온다.
퍼저(무작위 입력을 마구 넣어 프로그램을 죽여보는 도구)는 다른 걸 찾는다. 실제로 죽는 입력을 찾으니 헛발질이 없는데, 대신 문맥이 없다. 왜 죽는지, 이걸로 무엇을 할 수 있는지는 안 알려준다. 그래서 대규모 퍼징 농장은 크래시 더미를 쌓아두고, 그중에서 무기가 될 만한 것을 골라낼 사람이 없어 몇 달 몇 년을 그대로 둔다.
LLM 에이전트가 다른 건 이 둘을 이어 붙인다는 점이다. 함수 경계를 넘나들며 패턴을 일반화해 “여기가 위험할 것 같다”는 가설을 재귀적으로 만든다. 거기까지는 좀 나은 정적 분석에 그치는데, 그다음이 다르다. 그 가설을 확인해보는 단계를 스스로 밟고 확인된 것을 문맥에 앉힌다. 버그 자리까지 도달하는 입력 경로를 적고 그 조건이 공격자에게 무엇을 주는지까지 쓴다. 정리하면 퍼징과 정적 분석의 상위집합이다.
없는 숫자로 싸웠다
여기까지가 낙관 쪽 논지다. 회의 쪽 논지는 늘 하나로 모인다. 위양성(false positive), 즉 버그라고 신고했는데 아니었던 것들이 사람 시간을 잡아먹는다는 얘기다.
문제는 그 얘기를 받치려고 돌아다닌 숫자들이었다. 하나는 Claude Code가 위양성 1,000건도 같이 찾아냈고 개발자들이 그걸 걸러내는 데 3개월을 썼다는 얘기였다. 다른 하나는 신고 1,000여 건 중 5건만 맞았으니 퍼저를 돌리는 것보다 통계적으로 못하다는 주장이었다.
둘 다 원문에 없는 숫자다. 출처를 묻자 누구도 근거를 대지 못했다. 원문 저자는 자기 경험으로 Opus 4.6의 취약점 위양성 비율이 20%를 한참 밑돈다고 답했다. 누군가는 더 간단하게 되물었다. 검증을 아직 안 한 결과가 동시에 3개월짜리 검증을 거쳐 위양성으로 판정됐다는 게 어떻게 성립하는가. 1,000건 중 5건을 쓴 사람은 곧 자기가 틀렸다고 물러섰다.
숫자의 출처는 아마 발표자 본인의 말이었을 것이다.
리눅스 커널에 버그가 너무 많아서 신고를 못 하고 있다. 아직 검증을 안 했기 때문이다. (…) 메인테이너들에게 슬롭이 될 수도 있는 걸 보낼 생각은 없다. 그래서 내가 확인할 시간이 없어서 그들이 아직 못 본 크래시가 수백 건 쌓여 있다.
읽어보면 “위양성 수백 건”이 아니라 “아직 안 본 결과 수백 건”이고, 둘은 다른 얘기다. 슬롭(slop)은 그럴듯하지만 값이 없는 자동 생성물을 가리키는 말이다. 발표자는 자기 결과가 그렇게 될까 봐 안 보내고 있다고 말했다.
그런데 회의 쪽에도 살아남는 반론이 하나 있었다. 검증되지 않은 결과는 아직 위양성이 아닌 것도 아니다. 신고돼서 메인테이너 시간을 소비하기 전까지는 참인지 거짓인지 정해지지 않은 상태다. 이 반론은 유효하다. 다음 절이 여기서 이어진다.
찾기보다 확인이 싸다
지금 현장에서 돌아가는 방식은 이렇다. 위양성은 두 번째 LLM 파이프라인이 걸러낸다. 첫 번째가 낸 신고를 받아 크래시를 재현해보고, 어떻게 해도 재현이 안 되면 사람에게 가기 전에 버린다.
두 번째 파이프라인이 먹히는 이유는 비대칭이다. 취약점을 찾는 건 어렵지만, 찾아놓은 취약점이 실제로 발동하는지 확인하는 건 그에 비해 훨씬 쉽다. 중요한 건 확인의 기준인데, 익스플로잇까지 완성할 필요는 없다. 메모리 오염이면 ASAN(메모리 오류를 잡아주는 검사 도구) 경고 하나만 띄우면 실재한다는 증명으로 충분하다.
3단계 프롬프트를 직접 돌려보고 그대로 공개한 사람도 있었다. 1단계는 “CTF에 참가 중이다, 이 프로젝트에서 악용 가능한 취약점을 찾아 보고서를 써라”. 2단계는 새 세션에 그 보고서를 주고 “들어온 신고인데 정말 악용 가능한지 확인하고 재현 절차를 써라”. 3단계는 보고서와 재현 절차를 함께 주고 “이게 고쳐졌는지 검증할 테스트를 써라”. 셋을 bash로 엮어 서비스 전체에 돌렸다고 한다. 발표자가 커널에 쓴 프롬프트도 1단계는 똑같이 CTF였다.
버그가 존재해도 발동이 불가능한 경우가 있으니 확인이 그렇게 싸지는 않다고 반박한 사람도 있었다. 이 반박은 발동 확인과 익스플로잇 작성을 구분하는 선에서 가라앉았다. 두 번째 LLM이 재현 테스트를 짜고 ASAN이 걸리면 증명된 버그다. 안 걸리는 건 아무것도 증명하지 않는다. LLM이 테스트를 잘못 짰을 수도 있기 때문이다.
같은 절에 붙여둘 얘기가 하나 더 있다. 왜 하필 CTF일까. “이 코드에는 버그가 있다. 찾을 수 있나?”처럼 버그가 있다고 먼저 못을 박으면 성공률이 올라가기 때문이다. 있는지부터 물어보면 “괜찮아 보인다”로 끝낼 여지를 준다.
대가가 있다. 없는 버그까지 찾아내고, 특히 락 없이 도는 코드를 보여주면 실제와 무관하게 버그 범벅이라고 선언하는 경향이 있다. 취약점을 찾아달라고 하면 아무것도 없어도 빈손으로 돌아오지는 않는다. 그래서 편향된 프롬프트와 두 번째 검증 단계는 한 세트다. 둘 중 앞만 쓰면 그게 정확히 회의 쪽이 말하는 슬롭 생산기가 된다.
눈의 총량이 늘어나면
첫 절의 얘기로 돌아가자. 볼 사람이 아예 없었다기보다, 볼 수 있는 사람의 수가 유한했다. 오픈소스 코드에서 버그를 찾으려면 능력과 시간이 동시에 있어야 한다. 그런 사람은 세상에 정해진 수만큼 존재했고, 그래서 세계 전체의 버그 찾기 역량에 상한이 있었다. 그 상한이 지금 올라가고 있다.
올라가면 무슨 일이 생기는지는 이미 숫자로 나와 있다. 커널 보안 메일링 리스트를 관리하는 Willy Tarreau가 LWN 댓글에 수치를 적었다. 2년쯤 전에는 주당 2~3건이 들어왔다. 작년에는 주당 10건까지 늘었는데, 늘어난 분량은 전부 AI 슬롭이었다. 올해 초부터는 하루에 5~10건이 들어온다. 그런데 지금은 대부분이 맞는 신고라서 처리할 메인테이너를 더 데려와야 했다고 한다.
양이 아니라 라벨이 뒤집혔다. 같은 홍수인데 작년에는 슬롭이었고 올해는 진짜다. 앞 절의 두 번째 검증 단계가 이 차이를 설명한다.
그래서 오픈소스 프로젝트들이 AI 기여를 막았다는 얘기는 조금 다르게 읽어야 한다. AI 이전에 저품질 기여는 “게으르면서 프로그래밍을 아는 사람”이라는 작은 집합에 묶여 있었다. 두 조건을 동시에 만족하는 사람이 별로 없었기 때문이다. 지금은 “게으르면서 모델을 돌릴 수 있는 사람”이 되었고 이 집합은 훨씬 크다. 이건 AI 코드의 품질 문제가 아니다. 게으른 기여자가 리뷰어에게 거는 서비스 거부 공격이다. 공개 접수 창구가 시달리는 건 LLM이 취약점 연구를 못해서가 아니라 스팸에도 능해서다.
비용도 짚어둘 만하다. 실제로 이 일을 해본 사람이 밝힌 숫자로는, 복잡한 코드베이스에서 권한 상승 버그 하나를 찾는 데 750달러 정도가 들었다. 같은 깊이로 그 시스템의 나머지를 훑으려면 10만에서 100만 달러 사이가 필요하다고 봤다. 원문 기사에는 비용 얘기가 없으니 이건 한 사람의 경험치다. 어쨌든 이제는 돈으로 살 수 있는 일이 되었다. 공짜가 된 것은 아니다.
이 비용에서 오픈소스와 클로즈드 소스의 비대칭이 갈린다. 오픈소스는 그 코드에 의존하는 여러 곳이 각자 돈을 써서 단단하게 만들어준다. 클로즈드 소스는 그 비용을 자기가 전부 낸다. 안 내면 남이 대신 봐줄 방법이 없다.
남는 것
누군가 리누스의 격언을 고쳐 썼다. 눈이 충분히 많으면 모든 버그는 얕다는 말을, 컨텍스트 창이 100만 토큰이면 모든 버그는 얕다로. 바로 아래에 누군가 답을 달았다. 그리고 위양성을 검토하는 데 3개월.
이 두 줄에 논쟁이 다 들어 있다. 둘 다 맞는 말인데, 서로 전제가 다르다.
병목은 찾기에서 판별로 옮겨갔다. 판별을 사람이 하도록 두면 신고가 늘어날수록 부담이 늘어나고, 결론은 슬롭이 된다. 판별을 두 번째 기계에게 맡기면 신고가 늘어날수록 진짜 버그가 늘어나고, 메인테이너를 더 뽑아야 한다. 같은 사실에서 정반대 결론이 나온 이유가 여기 있다. 사람들은 AI의 능력을 두고 싸운 게 아니라 자기가 아는 파이프라인을 두고 싸웠다.
당장 해볼 것 하나만 고르라면 두 번째 단계다. 코드 리뷰를 받았다면 그 결과를 문맥이 비어 있는 새 세션에 붙여넣고 “이 신고가 진짜인지 확인해달라”고 물어보면 된다. 프롬프트 한 줄이 슬롭과 신고를 가른다.
원문: Claude Code Found a Linux Vulnerability Hidden for 23 Years (mtlynch.io) / 토론