Skip to content

Latest commit

 

History

History
252 lines (202 loc) · 15.6 KB

File metadata and controls

252 lines (202 loc) · 15.6 KB

KVM shadow paging의 페이지 역할 불일치로 인한 Use-After-Free (CVE-2026-53359)

메타데이터

항목 내용
CVE ID CVE-2026-53359
영향 소프트웨어 Linux kernel KVM/x86 shadow MMU
영향 버전 NVD의 kernel.org CNA 데이터 기준 Linux 2.6.36 이후 수정 커밋 반영 전; 배포판 커널은 backport 여부 확인 필요
CVSS v3.1 8.8 (High) — AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H (kernel.org CNA)
공개일 2026-07-04 (NVD)
취약점 유형 Use-After-Free; stale reverse-mapping entry가 해제된 shadow page의 SPTE를 가리킴
수정 커밋 81ccda30b4e83d8f5cc4fd50503c44e3a33abfeb
수정 파일 arch/x86/kvm/mmu/mmu.ckvm_mmu_get_child_sp()

개요

KVM/x86의 shadow MMU는 게스트 페이지 테이블을 읽어 호스트 커널 안에 shadow page table을 만든다. 게스트가 2MiB huge-page 매핑을 사용하다가 그 PDE를 바꾸면, 같은 GFN(Guest Frame Number) 위치에 기존 shadow page와 성격이 다른 새 shadow page가 필요할 수 있다.

CVE-2026-53359는 새 child shadow page를 찾는 kvm_mmu_get_child_sp()가 GFN만 비교하고 페이지의 전체 role은 비교하지 않아, 2MiB direct shadow page를 4KiB indirect shadow page가 재사용하는 문제다. 이후 4KiB leaf SPTE의 reverse-mapping(rmap)은 실제 변환 경로의 GFN에 기록되지만, 잘못 재사용된 부모가 삭제될 때는 sp->gfn + index로 다른 GFN을 계산해 그 rmap을 지우지 못한다. memslot 삭제로 shadow page가 해제된 뒤에도 남은 rmap을 dirty logging이나 MMU notifier가 따라가면서 해제된 페이지의 SPTE를 역참조해 Use-After-Free가 발생한다.

사전 지식

KVM shadow MMU는 게스트 페이지 테이블의 호스트 측 복사본이다

게스트 운영체제는 자체 페이지 테이블로 가상 주소를 게스트 물리 주소로 변환한다. KVM은 하드웨어가 지원하는 변환 기능만으로 끝나지 않는 경우, 게스트 페이지 테이블을 해석한 결과를 호스트 커널의 shadow page table에 반영한다. 게스트가 주소를 접근하면 KVM은 필요한 shadow 페이지 테이블 노드를 만들고, 그 노드의 SPTE(Shadow Page Table Entry)를 채운다.

이 문서에서 중요한 것은 게스트 페이지 테이블의 현재 모양과 shadow page의 생성 당시 모양이 함께 보존되어야 한다는 불변식이다. 같은 시작 GFN이라도 하나는 2MiB huge-page를 직접 매핑하는 노드이고, 다른 하나는 여러 4KiB 페이지를 따라가는 노드일 수 있다. 따라서 shadow page의 식별자는 GFN 하나가 아니라 role을 포함해야 한다.

GFN과 shadow page의 role

GFN은 게스트 물리 메모리의 페이지 번호다. 반면 kvm_mmu_pagerole은 shadow page가 어떤 방식으로 만들어졌는지를 나타내는 비트 묶음이다. 이번 경로에서 핵심 필드는 다음과 같다.

  • gfn: shadow page가 대표하는 게스트 프레임
  • direct: 게스트의 huge-page 매핑을 그대로 따라가는 direct shadow인지 여부
  • role.word: direct 외에도 레벨·접근 권한 등 shadow page의 구조적 속성을 묶은 값

2MiB PDE가 가리키는 동안 만들어진 shadow page는 direct=1이 될 수 있다. PDE가 외부에서 4KiB 페이지 테이블을 가리키도록 바뀌면 새 경로는 direct=0이어야 한다. 두 페이지는 GFN이 같아도 같은 객체가 아니다.

rmap은 “이 게스트 프레임을 누가 가리키는가”를 역방향으로 찾는 표다

KVM은 메모리 슬롯을 제거하거나 dirty logging을 수행할 때, 특정 GFN을 가리키는 shadow SPTE들을 찾아 수정·삭제해야 한다. 이를 위해 SPTE에서 GFN을 다시 찾아가는 대신, GFN마다 연결된 rmap 목록에 SPTE 위치를 기록한다.

정상적인 수명 규칙은 다음과 같다.

shadow page 생성
      │
      ├─ SPTE 설치 ──▶ 해당 GFN의 rmap에 SPTE 주소 등록
      │
      ├─ SPTE 제거 ──▶ 같은 rmap에서 SPTE 주소 제거
      │
      └─ shadow page 해제 ──▶ 어떤 rmap도 해제된 page 안의 SPTE를 가리키지 않음

이번 취약점은 마지막 규칙이 깨진 사례다. shadow page가 사라진 뒤에도 rmap에 SPTE 주소가 남아 있으면, 나중의 관리 작업이 그 주소를 정상 포인터로 오인한다.

memslot과 게스트 페이지 테이블 변경의 관계

KVM의 memslot은 게스트 물리 주소 범위를 호스트 메모리에 연결한다. memslot을 제거하면 그 범위에 속한 shadow 상태도 정리해야 한다. 동시에 게스트 페이지 테이블은 게스트 내부 코드가 아닌 외부 동작으로도 바뀔 수 있다. 이 둘이 결합되면 “현재 shadow page가 어떤 변환 경로를 표현하는가”를 정확히 보존하지 못하는 코드가 stale rmap을 만들 수 있다.

취약점 분석

패치 전: kvm_mmu_get_child_sp()가 GFN만 확인

수정 커밋의 부모 버전에서 child shadow page 재사용 조건은 child의 GFN만 비교했다. 핵심 코드는 다음과 같은 형태였다.

static struct kvm_mmu_page *kvm_mmu_get_child_sp(struct kvm_vcpu *vcpu,
        u64 *sptep, gfn_t gfn, bool direct, unsigned int access)
{
        union kvm_mmu_page_role role;

        if (is_shadow_present_pte(*sptep) && !is_large_pte(*sptep) &&
            spte_to_child_sp(*sptep) &&
            spte_to_child_sp(*sptep)->gfn == gfn)
                return ERR_PTR(-EEXIST);

        role = kvm_mmu_child_role(sptep, direct, access);
        return kvm_mmu_get_shadow_page(vcpu, gfn, role);
}

이 조건은 “현재 SPTE가 가리키는 child의 GFN이 요청 GFN과 같으면 같은 child다”라고 판정한다. 그러나 direct=1인 2MiB 경로와 direct=0인 4KiB 경로는 GFN이 같아도 role.word가 달라야 한다. GFN을 주소의 이름으로만 보고 페이지의 종류를 확인하지 않은 것이 첫 번째 깨진 불변식이다.

재현 가능한 상태 전이

공개 분석과 kernel.org의 수정 설명이 제시하는 핵심 전이는 다음과 같다. 여기서 “외부에서 PDE 변경”은 게스트 페이지 테이블이 VM entry 사이에 바뀌는 상황을 뜻한다.

┌──────────────────────────────────────────────────────────────────┐
│ 패치 전 KVM shadow MMU                                          │
├──────────────────────────────────────────────────────────────────┤
│ 1. 게스트 PDE = 2MiB huge mapping                               │
│    → shadow page S는 direct=1, 기준 gfn=G                         │
│                                                                  │
│ 2. PDE를 외부에서 4KiB page-table 경로로 변경                     │
│    → 새 경로는 direct=0이어야 함                                 │
│                                                                  │
│ 3. 게스트가 새 4KiB 페이지를 접근                                │
│    → get_child_sp(): gfn만 같음 → S를 새 child로 잘못 재사용      │
│    → leaf SPTE의 rmap은 실제 walk가 찾은 GFN에 기록               │
│                                                                  │
│ 4. 잘못 재사용된 child를 zap                                     │
│    → 부모 S가 direct=1인 채로 4KiB leaf를 정리                    │
│    → gfn 계산이 shadowed_translation[]가 아닌 S->gfn + index 사용 │
│    → 실제 leaf rmap을 찾지 못해 rmap entry 잔존                   │
│                                                                  │
│ 5. memslot 삭제                                                   │
│    → S 해제                                                        │
│    → 잔존 rmap ────────────────┐                                   │
│                               ▼                                   │
│ 6. dirty logging/MMU notifier가 rmap을 순회                       │
│    → 해제된 S 안의 SPTE 주소 역참조 → Use-After-Free               │
└──────────────────────────────────────────────────────────────────┘

왜 이전의 GFN 수정만으로 충분하지 않았는가

관련 선행 수정 커밋 0cb2af2ea66ad는 게스트 PDE 변경 뒤 저장된 GFN과 계산된 GFN이 달라지는 경로를 보완했다. 하지만 이번 경로에서는 GFN을 같게 만들 수 있다. 공격 또는 비정상 상태가 바꾼 PDE가 non-leaf page를 가리키면, 주소 번호인 GFN 비교는 통과하지만 페이지의 역할은 달라진다.

즉 검사는 다음 두 단계여야 했다.

요청 child와 기존 child 비교
        │
        ├─ GFN이 다른가?       ──▶ 재사용 금지
        ├─ role.word가 다른가? ──▶ 재사용 금지
        └─ 둘 다 같은가?       ──▶ 기존 child 재사용 가능

패치 전 코드는 첫 번째 줄만 검사했다. 그래서 “주소는 같지만 페이지 테이블 구조는 다른 객체”를 같은 객체로 취급했고, 이후 삭제·rmap 정리 단계에서 서로 다른 GFN 계산 규칙이 충돌했다.

보안 영향

가장 직접적인 결과는 호스트 커널의 Use-After-Free와 서비스 거부(호스트 커널 패닉)다. 연구자 공개 분석은 KVM/x86 게스트에서 게스트 측 동작만으로 shadow page 상태를 오염시킬 수 있다고 설명하며, 중첩 가상화와 신뢰하지 않는 게스트를 받는 호스트에서 게스트-호스트 격리 위험을 지적한다. 공개 재현 코드는 실행하지 않았으므로, 여기서는 소스·패치·공개 분석으로 확인되는 메모리 수명 위반까지만 단정한다. 실제 임의 코드 실행이나 안정적인 호스트 탈출 가능성은 커널 버전, 하드웨어 경로, 배포판 backport와 완화 기능에 따라 별도 확인이 필요하다.

수정 방법

수정 커밋은 role을 비교 대상으로 미리 계산하고, 기존 child의 role.word가 현재 요청의 role과 같은 경우에만 재사용하도록 조건을 확장했다.

 static struct kvm_mmu_page *kvm_mmu_get_child_sp(struct kvm_vcpu *vcpu,
         u64 *sptep, gfn_t gfn, bool direct, unsigned int access)
 {
-        union kvm_mmu_page_role role;
+        union kvm_mmu_page_role role = kvm_mmu_child_role(sptep, direct, access);

         if (is_shadow_present_pte(*sptep) && !is_large_pte(*sptep) &&
-            spte_to_child_sp(*sptep) && spte_to_child_sp(*sptep)->gfn == gfn)
+            spte_to_child_sp(*sptep) &&
+            spte_to_child_sp(*sptep)->gfn == gfn &&
+            spte_to_child_sp(*sptep)->role.word == role.word)
                 return ERR_PTR(-EEXIST);

-        role = kvm_mmu_child_role(sptep, direct, access);
         return kvm_mmu_get_shadow_page(vcpu, gfn, role);
 }
┌──────────────────────────────────────────────────────────────┐
│ 패치 후 child shadow page 선택                               │
├──────────────────────────────────────────────────────────────┤
│ 현재 PDE walk                                                  │
│   ├─ gfn = G                                                    │
│   └─ role = { direct=0, ... }                                   │
│                         │                                       │
│                         ▼                                       │
│ 기존 child = { gfn=G, role={ direct=1, ... } }                  │
│                         │                                       │
│              GFN 같음? YES                                     │
│              role.word 같음? NO                                │
│                         │                                       │
│                         └─ 기존 child 재사용 거부               │
│                            → 요청 role로 새 shadow page 생성     │
│                            → 설치·zap 시 rmap 계산 규칙 일치     │
│                            → memslot 삭제 전에 rmap 제거 가능   │
└──────────────────────────────────────────────────────────────┘

운영자는 실행 중인 KVM 호스트의 커널이 수정 커밋 또는 배포판 보안 업데이트를 포함하는지 확인하고 커널을 업데이트한 뒤 재부팅해야 한다. NVD의 kernel.org CNA 데이터는 mainline 수정 커밋과 여러 stable 커밋을 함께 참조하며, 배포판별 고정 버전은 동일한 upstream 버전 번호가 아닐 수 있다. 신뢰하지 않는 게스트를 운영하고 중첩 가상화를 외부에 노출하는 환경이라면 업데이트 전까지 중첩 가상화와 /dev/kvm 접근 정책을 재검토할 수 있지만, 이 조치는 공식 패치를 대체하지 않는다.

PoC 관련 언급

연구자가 공개한 호스트 DoS 재현 코드가 있음을 확인했지만, 이 자동화의 원칙에 따라 코드를 실행하지 않고 공개 설명과 소스·패치의 정적 관계만 분석했다. 따라서 보고서의 결론은 재현 성공 여부가 아니라 role 비교 누락 → 잘못된 shadow page 재사용 → stale rmap → 해제 페이지 역참조라는 코드 경로에 한정한다.

미해결/불확실 지점

  • NVD는 kernel.org CNA의 영향을 Git 커밋 범위와 일부 stable 버전으로 제시한다. 실제 운영 커널이 영향을 받는지는 배포판의 backport와 KVM 설정을 확인해야 한다.
  • 공개 자료는 Use-After-Free와 호스트 DoS 가능성을 충분히 설명하지만, 모든 Intel/AMD 조합에서 동일한 안정성으로 임의 코드 실행 또는 게스트 탈출이 가능한지는 이 보고서의 정적 분석만으로 확정하지 않는다.
  • 이 취약점은 shadow paging 경로의 문제다. 하드웨어 지원 페이지 테이블 경로가 항상 같은 shadow MMU 경로를 사용하는지는 CPU 기능과 KVM 설정에 따라 달라질 수 있다.

참고 자료