Skip to content

Guard pages

Linear bir overflow'un komşu mapping'i sessizce corrupt etmek yerine fault vermesi için stack'lerin ve allocation'ların yanına yerleştirilen unmapped / PROT_NONE page'ler.

Mechanism

Bir guard page, büyüyebilen veya overflow olabilen bir region'a bitişik yerleştirilen, read/write/execute permission'ı olmayan page boyutunda bir deliktir. Page un-addressable olduğu için, korunan region'ın sonunu geçen ilk sıralı erişim komşu mapping'e düşmek yerine bir fault alır (userspace'te SIGSEGV, kernel'de bir oops/panic).

Kanonik kernel kullanımı stack guard gap'tir. Orijinal stack guard page (CVE-2010-2240 için eklendi) stack VMA'sı ile komşusu arasına tek bir 4 KiB trap koydu. Stack Clash (CVE-2017-1000364) bir page'in çok küçük olduğunu gösterdi: büyük stack frame'leri allocate ederek bir program 4 KiB gap'i atlayabilirdi, böylece stack'e bir erişim aslında bitişik mapping'e (tipik olarak heap) düşüyordu; bu da guard page'e hiç dokunmadan controlled corruption ve privilege escalation veriyordu.

Invariant a guard page enforces

Fix (commit 1be7107fbe18, "mm: larger stack guard gap, between vmas"), gap'i stack VMA'sının içinde değil VMA'lar arasında boş alan olarak tutar — ona uymak zorunda olan birkaç yerde, başlıca arch_get_unmapped_area() ve VMA tree'sinin subtree-gap desteğinde, vm_start yerine vm_start_gap() (ve VM_GROWSUP için vm_end_gap()) kullanır. Gap default olarak 256 page (4 KiB-page sistemlerde ~1 MiB)'dir ve stack_guard_gap= boot parametresiyle (page birimi olarak) ayarlanabilir.

Invariant: stack asla başka bir mapping'in stack_guard_gap page'i içine kadar büyüyemez, dolayısıyla bir komşuya ulaşmak için gap'ten büyük bir frame gerekir — ve tek bir linear walk ulaşmadan çok önce fault verir.

İlgili guard region'lar aynı "komşuya ulaşmadan önce fault" özelliğini başka yerlerde de uygular: glibc her thread stack'ini bir guard region'la çevreler (pthread_attr_setguardsize) ve kernel'in VMAP_STACK'i her thread'in kernel stack'ini vmalloc alanında her iki tarafta unmapped guard page'lerle allocate eder, böylece bir kernel-stack overflow'u komşu kernel belleğini ezmek yerine fault verir.

Walkthrough

1. Mevcut stack guard gap'ini oku. Default'tan büyük bir gap, stack ile onun altındaki mapping arasında bir delik olarak görünür:

$ cat /proc/self/maps | tail -3
7ffd1c2a1000-7ffd1c2c2000 rw-p 00000000 00:00 0   [stack]
7ffd1c3f0000-7ffd1c3f4000 r--p 00000000 00:00 0   [vvar]
...

[stack]'in hemen altındaki unmapped span, guard gap'tir; stack allocator onun stack_guard_gap page'i içine başka bir VMA yerleştirmeyi reddeder.

2. Guard'ın davranışını gözle (fixed hâl). Eski sistemlerde küçük guard'lar (tek page) büyük allocation'larla atlanabilirdi; modern sistemlerde ~1 MiB gap ile basit bir 4 KiB jump hâlâ guard gap'e düşer ve fault verir. Onu atlamak için gap'ten büyük bir frame gerekir — onsuz, gap'e yapılan ilk probe fault verir:

/* touch memory one page below the current stack pointer */
volatile char *p = (char *)&p - 4096;
*p = 0;     /* lands in the guard gap -> SIGSEGV */
$ ./probe
Segmentation fault (core dumped)
Inspecting a thread stack guard with glibc
pthread_attr_t a; size_t g;
pthread_attr_init(&a);
pthread_attr_getguardsize(&a, &g);
/* g is the per-thread guard region size, default one page */

A guard page only stops linear overruns

Tek bir un-addressable page sıralı bir walk'ı durdurur, ama onu atlayan bir overflow (Stack Clash) veya attacker-controlled offset'li bir arbitrary write guard'ın ötesine düşebilir. Gap'in bir page'te bırakılmak yerine 4 KiB'tan ~1 MiB'a genişletilmesinin nedeni tam olarak budur.

Mitigation

Guard page'leri kendileri bir mitigation'dır; heap object'leri için init-on-alloc / init-on-free ve slab hardening ile, kernel stack'leri için ise VMAP_STACK ile birleşir. stack_guard_gap'i default'unda (veya daha büyük) tut ve CONFIG_VMAP_STACK ile build edilmiş kernel'leri tercih et.

References