Skip to content

mremap() TLB issue (CVE-2018-18281)

Linux 3.2'den beri mremap(), TLB'yi page-table lock'u bıraktıktan sonra flush'lıyordu; eşzamanlı bir ftruncate() o pencere içinde bir page'i free edebilir ve geride, artık free edilmiş (ve yeniden tahsis edilmiş) physical page'i hâlâ map'leyen stale bir TLB entry bırakır — userspace'ten erişilebilen physical memory'nin bir use-after-free'i.

Mechanism

Neden çalışır

TLB, virtual→physical translation'ları cache'ler. Kernel'in page-table entry'lerini taşımak/kaldırmak için uyguladığı güvenlik kuralı şudur: PTE'yi kaldır, physical page'e bir reference tut, TLB'yi flush'la ve ancak ondan sonra reference'ı bırak. Eğer page reference'ını (page'in free edilmesine izin vererek) TLB'yi flush'lamadan önce bırakırsan, eski translation'ı hâlâ cache'inde tutan herhangi bir CPU, frame geri dönüştürüldükten sonra o physical frame'i okuyabilir/yazabilir.

mremap() bir VMA'nın PTE'lerini relocate eder:

mremap() -> move_vma() -> move_page_tables() -> move_ptes()

move_ptes(), her source PTE'yi atomik olarak read-and-clear etmek için ptep_get_and_clear() kullanır, onu destination'a yazar, ardından TLB'yi flush'lamadan önce page-table lock'u bırakıyordu. Kritik nokta şu: mremap() yalnızca mmap_sem tutar — bu da preemption'ı engellemez ve page-cache truncation path'ini bloklamaz.

Bir dosyayı küçülten eşzamanlı bir ftruncate(), aynı address space'in page table'larını yalnızca page-table lock'ları tutarak (yani mmap_sem tutmadan) dolaşır. Dolayısıyla ftruncate(), mremap() ile race'e girebilir: mremap()'in source PTE'lerden zaten temizlediği ama henüz TLB'den flush'lamadığı page'leri kaldırır/free eder. Page, per-CPU page allocator'a geri döner ve yeniden tahsis edilir; bu sırada race'e giren CPU üzerindeki stale bir TLB entry hâlâ ona işaret eder.

Bu stale entry üzerinden userspace şunları yapabilir:

  • free edilmiş page'in yeniden kullanıldıktan sonraki yeni içeriğini okuyabilir (info leak), ve
  • şimdi frame'e sahip olan nesneyi bozmak için onun üzerinden yazabilir.

Pencere normalde çok küçüktür, ancak CONFIG_PREEMPT kernel'lerde (örneğin Pixel 2'nin 4.4 branch'i) task, unlock'tan sonra ama flush'tan önce preempt edilebilir; bu da pencereyi milisaniyelere genişleterek deterministik biçimde sürülecek kadar büyütür. Jann Horn bug'ı raporladı ve test etti.

Walkthrough

Yalnızca yetkili test

Yalnızca sahip olduğun bir kernel/VM üzerinde reproduce et. Aşağıdaki teknik, Project Zero'nun analizini ve PoC stratejisini başka kelimelerle aktarır; bu bir race olduğu için başarı scheduling'e bağlıdır.

1. Flush'lanmamış TLB penceresini belirle. Açığa yol açan sıralama move_ptes() içindedir: PTE'ler temizlenip yeniden yerleştirilir, ardından lock bırakılır, sonra (çok geç olarak) TLB flush'lanır. İş, bu boşluk sırasında bir source page'i free etmektir.

2. Kontrollü preemption kur. Project Zero'nun PoC'si, race'e giren task'leri tek bir core'a pin'ler ve mremap() task'inin priority'sini düşürerek kolayca yield etmesini sağlar:

// pin processes to one CPU and run the mremap task at idle priority
cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set);
sched_setaffinity(0, sizeof(set), &set);
struct sched_param sp = {0};
sched_setscheduler(0, SCHED_IDLE, &sp);   // yields to any higher-prio task

3. Page-table allocation'ını procfs üzerinden tespit et. mremap()'in destination page table'larını ne zaman tahsis ettiğini (yani race için doğru fazda olduğunu) öğrenmek için VmPTE alanını poll et:

$ grep VmPTE /proc/self/status
VmPTE:        24 kB

4. Race'i tetikle. mremap() operasyon ortasında duraklatılmışken (unlock'tan sonra, flush'tan önce preempt edilmişken), source page'leri free etmek için onları besleyen dosya üzerinde ftruncate() çağır; tam o anda higher-priority task'i uyandırmak için bir pipe write kullanılır. Free edilmiş page daha sonra per-CPU freelist'in cold ucundan yeniden tahsis edilir.

5. Stale mapping'i tespit et ve frame'i yeniden kullan. Eski virtual address üzerinden tekrar tekrar oku; free edilmiş frame, hedef bir page-cache page'ine yeniden tahsis edildiğinde (Project Zero, zygote tarafından kullanılan libandroid_runtime.so'nun nativeForkAndSpecialize()'ini hedef aldı), stale TLB entry o page'i açığa çıkarır — ve üzerine yazmana izin verir — bu da privileged bir process'e code injection sağlar.

Observable success: reads through the old VA return contents that no longer
belong to the mremap'd region (the reused frame), and writes through it
persist into the reallocated page.

Detection

  • Race, tasarımı gereği sessizdir; instrumented kernel'lerde TLB state ile PTE state arasındaki uyumsuzluklar normalde raporlanmaz. Tespit, davranışsal düzeyde pratiktir: mremap(), ftruncate(), SCHED_IDLE affinity pinning ve /proc/self/status VmPTE polling'ini sıkı sıkıya iç içe geçiren unprivileged bir process anormal bir pattern'dir.
  • KASAN, yeniden kullanılan frame'in bazı reclaim path'lerinde kullanımını yakalayabilir.

Mitigation

  • Patch: upstream commit eb66ae030829605d61fbef1909ce310e29f78821 ("mremap: properly flush TLB before releasing the page") ile 4.9.135, 4.14.78, 4.18.16 ve 4.19'da düzeltildi. Fix, flush_tlb_range()'i destination page-table lock bırakılmadan önce çalışacak şekilde taşır:
// ... after moving the PTEs ...
if (force_flush)
    flush_tlb_range(vma, old_end - len, old_end);
if (new_ptl != old_ptl)
    spin_unlock(new_ptl);          // TLB now flushed BEFORE unlock

Jann Horn, flush'ın yalnızca source lock'tan değil, destination page-table lock'tan da önce gelmesi gerektiğine dikkat çekti. move_ptes(), move_huge_pmd() ve move_page_tables() buna göre düzenlendi. - Underlying TLB-flush-after-unlock davranışı Linux 3.2'den beri mevcuttu (NVD/oss-security), dolayısıyla 2016 öncesi kernel'ler de etkilenmiştir. Kasım 2016'daki 5d1904204c99 (ilk olarak v4.9) commit'i bug'ı getirmedi; yalnızca exploitability'yi değiştirdi: bu commit'ten önce race penceresi non-dirty PTE'ler için de use-after-free write'lara açıktı, sonrasında ise Dirty olmayan PTE'lerde pencere use-after-free read'lerle sınırlandı (write yalnızca Dirty PTE üzerinden). CVE, 4.9.135, 4.14.78, 4.18.16 ve 4.19'da düzeltildi.

References