← All reports

[libpas] Guard page at the front of the compact-heap reservation

Component: bmalloc libpas | ba26b52

Source/bmalloc/libpas/src/libpas/pas_compact_heap_reservation.c

- page_result = pas_page_malloc_try_allocate_without_deallocating_padding(
- pas_compact_heap_reservation_size, pas_alignment_create_trivial(), false);
+ page_result = pas_page_malloc_try_allocate_with_guard_pages_without_deallocating_padding(
+ pas_compact_heap_reservation_size, pas_alignment_create_trivial(), false,
+ pas_compact_heap_reservation_guard_size);
...
- pas_compact_heap_reservation_base =
- (uintptr_t)page_result.result - pas_compact_heap_reservation_guard_size;
- pas_compact_heap_reservation_available_size =
- pas_compact_heap_reservation_size - pas_compact_heap_reservation_guard_size;
+ pas_compact_heap_reservation_base = (uintptr_t)page_result.result;

Source/bmalloc/libpas/src/libpas/pas_page_malloc.c

+pas_aligned_allocation_result
+pas_page_malloc_try_allocate_with_guard_pages_without_deallocating_padding(
+ size_t size, pas_alignment alignment, bool may_contain_small_or_medium,
+ size_t guard_size)
+{
+ ...
+#if PAS_OS(WINDOWS)
+ PAS_ASSERT(VirtualProtect(result.result, guard_size, PAGE_NOACCESS, &old_protect));
+#else
+ PAS_SYSCALL(mprotect(result.result, guard_size, PROT_NONE));
+#endif
+ return result;
+}

libpas는 allocator metadata를 고정된 VA reservation 안에 채워 넣고, 각 object를 full pointer가 아니라 8바이트 정렬된 index로 참조합니다. 이렇게 하면 object 하나당 overhead를 줄일 수 있습니다.

Index 0과 1은 애초에 할당되지 않으며, compact memory의 첫 byte는 index 2를 통해 접근됩니다. 다만 이번 변경 전에는 이 reservation이 순전히 산술 계산으로만 정의되어 있었습니다. base가 mapped page에서 guard_size를 뺀 값으로 계산되었기 때문에, index 0을 디코딩하면 reservation 바깥, 즉 인접한 일반 VM 영역을 가리키게 되는 구조였습니다. 이번 patch는 첫 page를 PROT_NONE으로 mprotect하고 base를 mapped address 그대로 설정함으로써, 이 guard를 실제로 동작하게 만듭니다.

Before:                                    After:
compact idx 0 -> base-guard_size (mapped)  compact idx 0 -> base+0 (PROT_NONE)
                 corrupts neighbor VM                     SIGSEGV, deterministic
reservation_base = page - guard_size       reservation_base = page
available_size = size - guard_size         available_size removed; size unchanged

Zero로 초기화된 compact pointer가 검증 없는 _load_non_null accessor를 통해 디코딩되더라도, 이제는 인접 영역을 조용히 손상시키는 대신 항상 동일하게 fault가 발생합니다. 이로써 compact-pointer-corruption 계열의 버그는 exploitable한 primitive에서 디버깅 가능한 crash로 성격이 바뀝니다.

이번 변경은 새로운 attack surface가 아니라 다른 곳에 있는 버그에 대한 mitigation입니다. 다만 동시에, vendor가 어떤 지점을 exploitable하다고 판단하는지 보여주는 지도 역할도 합니다. 즉, compact pointer의 index가 검증 없는 _load_non_null 계열 accessor로 dereference되기 전에 0으로 초기화되거나 손상될 수 있는 모든 code path가 대상입니다.

좁게 보면, guard_size 한 page만으로 zero index 외에 여러 index만큼 벗어난 corruption까지 충분히 잡아낼 수 있는지 확인할 필요가 있습니다. 넓게 보면, 여전히 pas_page_malloc_try_allocate_without_deallocating_padding을 직접 호출해 guard를 완전히 우회하는 call site들을 점검해야 합니다. PLAYSTATION branch는 이번에도 guard가 적용되지 않은 채로 남아 있습니다. 더 이식성 있는 관점에서는, libpas 내 다른 모든 index-encoded pointer space를 나열하고 각각의 index-zero 디코딩이 mapped memory로 이어지는지 확인할 필요가 있습니다. 판별 기준은, base를 guard size만큼 빼서 계산하는 reservation인지, 아니면 실제로 page를 mapping해서 계산하는 reservation인지입니다.