커널 취약점은 가끔 너무 거창한 말로 들려. 그런데 GhostLock의 시작점은 작아. 잠든 스레드 하나의 대기 상태를 정리하면서, 커널이 지워야 할 사람의 포인터가 아니라 지금 실행 중인 사람의 포인터를 지웠다.1

그 결과는 작지 않았어. GhostLock(CVE-2026-43499)은 Linux 2.6.39 계열에서 들어와 7.1 쪽에서 수정된 futex 취약점이고, 비특권 로컬 사용자가 스택 use-after-free를 권한 상승과 컨테이너 탈출로 이어갈 수 있었다고 정리돼 있어.1

핵심은 "오래된 버그가 있었다"보다, 커널의 아주 좁은 정리 경로 하나가 공격자가 다시 붙잡을 수 있는 스택 포인터를 남겼다는 점이야.

어디서 틀렸나

문제는 futex의 Requeue-PI 프록시 경로에 있어. 원문은 이 경로에서 rt_mutex_start_proxy_lock()이 다른 잠든 태스크를 대신해 rt_mutex_waiter를 큐에 넣고, 오류가 나면 이를 되돌린다고 설명해.1

정상 경로에서 remove_waiter()는 실행 중인 current가 대기자의 소유 태스크라고 보고 current->pi_blocked_on을 비워. 그 상황에서는 맞는 정리야. 그런데 Requeue-PI 프록시 경로에서는 다르다. FUTEX_CMP_REQUEUE_PI를 호출한 requeuer가 다른 잠든 태스크의 waiter를 대신 큐에 넣고, 오류가 나면 되돌린다.1

여기서 -EDEADLK 오류가 나면 꼬리가 생겨. 커널은 실제로 잠들어 있던 waiter를 큐에서 빼지만, 그 waiter의 pi_blocked_on이 아니라 requeuer인 current의 값을 지워. 실제 waiter는 자기 커널 스택 안의 rt_mutex_waiter를 계속 가리킨 채 사용자 공간으로 돌아온다. 그 스택 프레임은 이미 끝난 호출의 자리야.1

나중에 PI 체인 탐색이 그 태스크를 지나가면, 커널은 끝난 스택 프레임을 다시 본다. use-after-free가 되는 지점이 여기야.

왜 이게 실제 공격이 됐나

이런 버그가 곧바로 root를 주는 건 아니야. GhostLock 분석에서 중요한 부분은 연구진이 이 dangling pointer를 제어 가능한 구조체처럼 다시 채웠다는 점이야.

트리거는 세 futex와 세 스레드로 만든 순환이야. waiter가 하나의 PI futex를 잡고 일반 futex에서 기다리고, owner는 다른 PI futex를 잡은 뒤 waiter가 가진 futex에서 막힌다. main 스레드가 requeue를 시도하면 의존성은 waiter에서 target futex, owner, chain futex, 다시 waiter로 닫힌다. 이때 PI 체인 탐색이 -EDEADLK를 반환하고 잘못된 롤백이 실행된다.1

그다음에는 스택 재사용이 붙어. waiter는 futex 호출에서 돌아온 뒤 PR_SET_MM_MAP 경로를 타고, 이 경로의 user_auxv 스택 버퍼가 이전 waiter 자리와 비슷한 깊이에 놓인다. 연구진은 그 공간에 가짜 rt_mutex_waiter를 배치해 커널이 다시 읽을 구조를 만들었다고 설명해.1

그래도 완전한 임의 쓰기는 아니야. 원문이 말하는 프리미티브는 제약된 포인터 쓰기와 제약된 8바이트 0 쓰기야. 포인터가 여러 번 역참조되고, rb-tree 삭제 조건과 주변 값 조건도 맞아야 한다. 공격이 성립하려면 스택 프레임 재사용, 가짜 waiter 검사 통과, 쓸 대상 주소 선정이 모두 맞아야 했다.1

컨테이너 탈출까지 간 이유

GhostLock의 무게는 이 제약을 실제 제어 흐름 탈취로 이어갔다는 데 있어. 분석은 prefetch timing으로 KASLR와 physmap 기준 주소를 찾고, CPU entry area(CEA)를 가짜 구조체와 ROP 스택을 놓는 작은 작업 공간으로 썼다고 설명해.1

그 뒤 inet6_protos[IPPROTO_UDP] 주변의 조건이 맞는 함수 테이블을 덮는다. IPv6 UDP 루프백 패킷을 보내면 커널이 가짜 handler를 호출하고, 짧은 피벗 뒤 DirtyMode 경로로 core_pattern 관련 접근 모드를 바꾼다. 이렇게 되면 사용자 공간의 helper를 루트 권한으로 실행하는 길이 열린다.1

이건 “컨테이너 보안 설정 하나가 허술했다”는 이야기가 아니야. 별도 권한이나 사용자 네임스페이스 없이 로컬 비특권 사용자가 트리거할 수 있는 커널 경로가 문제였고, 연구진은 Google kernelCTF 환경에서 약 5초 만에 플래그를 얻는 체인을 만들었다고 적었다. 안정성도 97%로 제시돼 있어.1

패치와 완화가 말해주는 것

최종 패치는 원인이 된 착각을 바로잡는다. current를 기준으로 정리하지 않고, waiter가 실제로 속한 태스크를 기준으로 pi_lock을 잡고 pi_blocked_on을 비운다. 후속 rt_mutex_adjust_prio_chain()에도 current 대신 waiter의 태스크를 넘기는 식이다.1

완화책도 경계가 분명해. RANDOMIZE_KSTACK_OFFSET를 켜면 해제된 waiter 프레임과 후속 user_auxv 프레임이 결정적으로 겹치는 단계가 약 1/32 확률의 추측 문제가 된다. STATIC_USERMODE_HELPER는 이 글에 나온 DirtyMode 경로를 막는다. 다만 원문은 예측 가능한 쓰기 가능 커널 데이터에 있는 다른 /proc/sys 설정으로 비슷한 방식을 일반화할 수 있다고도 말해.1

그래서 다음에 볼 것은 세 가지야. 내가 쓰는 배포판의 패치가 이 수정까지 들어왔는지, hardened 설정에서 kernel stack offset randomization 같은 완화가 켜져 있는지, 그리고 컨테이너를 “격리된 방”으로 볼 때도 결국 같은 커널을 공유한다는 사실을 운영 기준에 넣고 있는지.

GhostLock은 낡은 버전 번호 하나의 문제가 아니야. 잠금, 우선순위, 스택 재사용, 주소 누출, 함수 테이블, 사용자 공간 helper가 한 줄로 이어질 때 커널의 작은 정리 실수가 얼마나 멀리 갈 수 있는지를 보여주는 사례야.

각주

  1. GeekNews, 「GhostLock, 15년간 모든 Linux 배포판에 존재한 스택 UAF」(2026-07-14) 원문. ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8 ↩︎9 ↩︎10 ↩︎11 ↩︎12 ↩︎13