Skip to content
Published on

GhostLock: 리눅스 rtmutex에 15년을 숨어 있던 스택 use-after-free (CVE-2026-43499)

공유하기
Authors

들어가며 — 원인은 놀랄 만큼 작았다

2026년 7월 7일, nebusec.ai에 "IonStack part II"라는 제목의 분석 글이 올라와 해커뉴스에서 화제가 됐다. 이 글이 다루는 것이 GhostLock, 즉 CVE-2026-43499다. 리눅스 커널의 실시간 뮤텍스(rtmutex) 코드(kernel/locking/rtmutex.c)에 있던 스택 use-after-free이며, 연구진의 설명으로는 2011년 Linux 2.6.39-rc1부터 메인라인에 있었고 2026년 4월 7.1-rc1에서야 고쳐졌다. 대략 15년이다.

흥미로운 지점은 그 15년이라는 숫자가 아니다. 원인이 민망할 만큼 작다는 것이다. 자신을 호출하는 스레드가 누구인지 잘못 가정한 헬퍼 함수 하나가, 15년의 리뷰와 퍼징과 lockdep을 견디고 살아남았다. 이 글은 세 가지를 짚는다. 스택 use-after-free가 무엇이고 왜 미묘한지, 이렇게 작은 버그가 어떻게 그렇게 오래 숨는지, 그리고 의도적으로 과장을 걷어내고 GhostLock을 실제로 익스플로잇하려면 무엇이 필요하며 우리는 무엇을 해야 하는지다.

먼저 단서 하나. 이 글은 시리즈의 2부이고, 1부를 요약해 주지는 않는다. 나는 2부가 다룬 내용만 서술하며, 어떤 수치가 독립적으로 확인된 것이 아니라 이 한 편의 글에 근거하는지도 함께 밝힌다.

스택 use-after-free란 무엇인가

use-after-free는 말 그대로다. 메모리가 해제됐는데도 그 포인터를 계속 쓰는 것이다. 힙에서의 사례는 익숙하다. 스택 버전은 더 낯설다. 지역 변수는 함수의 스택 프레임에 산다. 함수가 반환되면 그 프레임은 "해제"되는데, 그 공간이 다음 호출에 곧바로 재사용될 수 있다는 의미에서다. 지역 변수를 가리키는 포인터가 그 변수를 소유한 함수보다 오래 살아남으면, 이후 코드는 이제 다른 무언가의 것이 된 메모리를 읽거나 쓸 수 있다.

커널에서는 이게 유저 공간보다 더 고약하다. 오염시킬 할당자 메타데이터도 없고 눈에 띄는 크래시도 없다. 스택은 그냥 RAM이고, 그 공간을 재사용하는 것이 정상적인 빠른 경로이기 때문이다. 해제된 스택 프레임을 가리키는 대롱거리는 포인터는, 누군가의 데이터가 마침 그 자리에 떨어지기 전까지는 아무 일도 하지 않는다.

그 침묵이 문제의 전부다. 이 버그가 평범한 테스트에서 보이지 않는 이유는, 대부분의 경우 해제된 프레임을 아직 아무도 덮어쓰지 않았기 때문이다. 공격자가 자신이 통제하는 바이트로 그 공간을 의도적으로 되찾아 채울 때 비로소 무기가 된다 — GhostLock 익스플로잇이 하는 일이 정확히 이것이다.

GhostLock의 실체

문제의 코드는 실시간 뮤텍스(rtmutex) 서브시스템이고, PI-futex 시스템콜을 통해 도달한다. 락을 쥔 저우선순위 스레드가 고우선순위 스레드를 굶기지 못하게 막는 우선순위 상속(priority inheritance) 기계다. 문제의 헬퍼는 remove_waiter()다. 원래 한 가지 상황을 위해 쓰였다 — 스레드가 락에서 블록됐다가 스스로 뒤처리를 하는 상황. 그래서 실행 중인 스레드(current)가 언제나 제거 대상 waiter라고 가정하고 current->pi_blocked_on을 지웠다.

requeue-PI 기능이 그 가정을 조용히 깨뜨렸다. FUTEX_WAIT_REQUEUE_PIFUTEX_CMP_REQUEUE_PI를 쓰면, 한 스레드의 rt_mutex_waiter(그 스레드의 스택에 산다)를 다른 스레드가 rt_mutex_start_proxy_lock()으로 또 다른 futex에 대리 등록한다. 이 경로가 데드락 사이클을 감지해 -EDEADLK로 롤백하면, 엉뚱한 스레드에서 remove_waiter()를 호출해 실제 waiter가 아니라 대리 등록자의 상태를 지운다. 그 결과 진짜 waiter는 이제 재사용된 자기 스택 프레임을 가리키는 포인터로 여전히 PI 체인에 묶인 채 유저 공간으로 돌아가고, 다음 체인 워크가 해제된 메모리를 역참조한다.

/* rtmutex.c: 헬퍼는 실행 중인 스레드가 곧 waiter라고 가정했다 */
static void remove_waiter(struct rt_mutex *lock, struct rt_mutex_waiter *w)
{
        /* ... 락의 waiter 트리에서 w를 떼어낸다 ... */
        current->pi_blocked_on = NULL;   /* 버그: w가 아니라 current를 지운다 */
}

/* 수정, 커밋 3bfdc63936dd: waiter 자신의 task를 대상으로 */
raw_spin_lock(&w->task->pi_lock);
w->task->pi_blocked_on = NULL;

수정, 커밋 3bfdc63936dd("rtmutex: Use waiter::task instead of current in remove_waiter()")는 본질적으로 한 줄이다. current 대신 waiter 자신의 task를 잡아, waiter->task->pi_lock을 걸고 waiter->task->pi_blocked_on을 지운다. 15년의 노출이 올바른 구조를 가리키는 것만으로 닫혔다 — 안심되면서도 조금 서늘한 사실이다.

15년을 버젓이 숨은 이유 — 그리고 익스플로잇의 실제 난이도

이 오래 산 이유에 대한 글의 설명은 평범하지만 설득력 있다. 가정의 경계를 넘은 함수 재사용이다. remove_waiter()는 원래 호출자에게는 옳았고 거기서는 계속 옳았다. requeue-PI가 current가 누구인지에 대한 관념이 다른 두 번째 호출자를 추가했는데, 아무도 그 가정을 다시 확인하지 않았다. 게다가 버그에 도달하려면 특정한, 무작위가 아닌 배치가 필요하다 — -EDEADLK 롤백을 강제하기 위해 세 개의 futex와 세 개의 스레드에 걸친 우선순위 상속 의존성 사이클을 만들어야 한다. 퍼저나 일상적인 워크로드가 좀처럼 만들지 않는 상태다.

이제 영향에 대한 정직한 부분이다. GhostLock은 로컬 권한 상승이지 원격 코드 실행이 아니다. 이 모든 것이 성립하려면 이미 그 머신에서 코드를 실행할 수 있어야 한다 — 셸이든, 컨테이너 안의 프로세스든. 익스플로잇도 한 줄짜리가 아니다. 공개된 체인은 prctl(PR_SET_MM_MAP)으로 해제된 프레임을 손질해 가짜 waiter를 위조하고, 제약된 rb-트리 쓰기로 CPU entry area에 있는 inet6_protos[IPPROTO_UDP]를 덮어쓴 뒤, 마지막에 /proc/sys/kernel/core_pattern의 권한 비트를 뒤집어 루트를 얻는다.

이건 연구급 작업이다. 저자들은 하드닝 옵션을 끈 kernelCTF 조건에서 97% 안정성을 보고했고, 구글 kernelCTF는 여기에 92,337달러를 지급했다. 공정하게 읽자면, 신뢰할 만하고 실재하지만 한계가 뚜렷하다 — 로컬 접근과 정교한 체인이 필요하다. 로컬 권한 상승이 하나의 침해된 워크로드를 노드 전체 장악으로 바꾸는 컨테이너 탈출이, 가장 걱정해야 할 시나리오다.

공개 과정도 빨랐고, 그 점은 수정 자체만큼 중요하다. 글에 따르면 버그는 2026년 4월 18일 security@kernel.org에 보고됐고, 4월 20일 메인라인에서 고쳐졌으며, 5월 4일 stable로 백포트됐다. 구글은 6월 30일 kernelCTF 제출을 인정했고, 공개 분석은 7월 7일에 나왔다. 조용히 고쳐진 시점과 공개적으로 상세히 밝혀진 시점 사이의 간격은 약 두 달이었다 — stable 사용자가 그 메커니즘이 1면에 오르기 전에 업데이트할 시간이 있었다는 뜻이다.

마무리 — 패치, 그리고 노출 여부 확인법

실무적 대응은 지루하지만 정확하다. 패치다. 수정은 메인라인에 커밋 3bfdc63936dd("rtmutex: Use waiter::task instead of current in remove_waiter()")로 들어갔고 2026년 5월 4일 stable 트리로 백포트됐으니, 최신으로 업데이트된 배포판 커널이라면 이미 담고 있다. 노출 범위는 원칙적으로 넓다 — CONFIG_FUTEX_PI=y가 켜진 2.6.39-rc1 이후 모든 커널, 사실상 모든 배포판 빌드에 도달한다. 그러니 진짜 질문은 "영향받는가"가 아니라 "패치됐는가"다.

uname -r                      # 실행 중인 커널
# 그다음 배포판의 CVE-2026-43499 권고를 확인한다.
# 2026-05-04 이후 업데이트된 stable 커널이면 패치된 것이다.

글에서 언급한 심층 방어 두 가지 — 어느 것도 패치를 대신하지 못한다. RANDOMIZE_KSTACK_OFFSET은 시스템콜마다 스택 오프셋을 무작위화해 익스플로잇이 의존하는 결정론적 프레임 겹침을 깬다. 약 5비트, 대략 32분의 1 확률에 불과하지만 없는 것보다는 낫다. STATIC_USERMODE_HELPER는 여기서 쓰인 특정 /proc/sys 경로를 막지만, 근본 버그 자체를 막지는 못한다. 그리고 수치에 대한 정직한 단서. 15년이라는 수명과 버전 세부 정보는 시리즈 2부에 해당하는 한 편의 벤더 글에서 나온다. 메커니즘은 구체적이고 커널 트리와 대조해 확인할 수 있어 나는 신뢰하지만, 주변 수치들은 독립 확인이 아니라 그 출처의 서술로 받아들이는 편이 낫다.

참고 자료