← 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 packs allocator metadata into a fixed VA reservation and references those objects by 8-byte-aligned index rather than by full pointer, to shrink per-object overhead. Indices 0 and 1 are never handed out — the first byte of compact memory is reached via index 2 — but before this change that reservation was purely arithmetic: the base was computed as the mapped page minus guard_size, so decoding index 0 pointed outside the reservation, into an adjacent ordinarily-mapped VM region. The patch mprotects the first page PROT_NONE and sets the base to the mapped address, making the guard real.

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

A zeroed compact pointer decoded through an unchecked _load_non_null accessor now faults deterministically instead of silently corrupting whatever is mapped next door. That converts a class of compact-pointer-corruption bugs from an exploitable primitive into a debuggable crash.

This is a mitigation for bugs elsewhere, not new surface — but it doubles as a map of what the vendor considers exploitable: any code path that can zero or corrupt a compact pointer's index before it is dereferenced through an unchecked _load_non_null-style accessor. Narrow: check whether a single page of guard_size is large enough to catch multi-index-off corruption, not just the zero case. Wider: audit the remaining call sites that still reach pas_page_malloc_try_allocate_without_deallocating_padding directly and therefore bypass the guard entirely — the PLAYSTATION branch is left unguarded — and, more portably, enumerate every other index-encoded pointer space in libpas and ask whether its index-zero decode lands on mapped memory. Match tell: a reservation whose base is computed by subtracting a guard size rather than by mapping one.