← All reports

[libpas] Use tagged_bmalloc_heap for MTE allocations

Component: libpas / bmalloc allocator | c8cb35b

Source/bmalloc/bmalloc/bmalloc.cpp

void freeOutOfLine(void* object, HeapKind kind)
{
+ if (bmalloc_try_deallocate_casual(object))
+ return;
+ // ... expected to never be hit in non-hardened WebContent processes
+ if (tagged_bmalloc_try_deallocate_inline_only(object))
+ return;
+ tagged_bmalloc_deallocate_casual(object);
}
+void* reallocOutOfLine(void* object, size_t newSize)
+{
+ if (tagged_bmalloc_owns_object(object))
+ return tagged_bmalloc_reallocate(object, newSize, pas_reallocate_free_if_successful);
+ return bmalloc_reallocate_inline(object, newSize, pas_reallocate_free_if_successful);
+}

Source/bmalloc/bmalloc/TZoneHeap.cpp

void tzoneFree(void* p)
{
- bmalloc_deallocate_inline(p);
+ api::free(p);
}

libpas is the allocator behind bmalloc, FastMalloc and TZoneMalloc. Until now a single pas_heap had to serve both MTE-tagged and untagged allocations, and "compact" allocations (pointers that fit a compressed 32-bit form) needed their own megapages and heap-refs. With ARM MTE, memory carries a 4-bit tag that is checked on access and re-randomized on free to catch use-after-free and overflows. Because every heap could tag, tag-setting code was inlined everywhere and heaps had to mmap different memory flavors.

This commit enables the previously compiled-in but unused tagged_bmalloc_heap for MTE-tagged FastMalloc allocations. Compactness is hoisted out of libpas: pas_allocation_mode is deleted and compact/non-compact megapages and heap-refs are collapsed. bmalloc.h dispatches between the tagged and default heaps. The free path speculates non-MTE first, then checks MTE, then falls through to the casual case. Tag-setting moves out of line behind the tagged heap, and the tagging decision moves from per-allocator is_mte_tagged to the heap config. Sufficiently large compact objects can now be delegated to libmalloc, and the small_medium_bootstrap heap files are renamed pas_mte_bootstrap_*.

Allocation (bmalloc::api::malloc, mode = Compact | NonCompact):

  malloc(size, mode)
    ├─► Compact ──► bmalloc_heap
    └─► NonCompact
          ├─► MTE enabled (hardened processes), taggable size
          │     └─► tagged_bmalloc_heap (tag-set / retag out of line)
          └─► MTE disabled, or above tagging ceiling
                └─► bmalloc_heap (no tag code inlined; WebContent fast path)

Free (bmalloc::api::free):
  Before: bmalloc_deallocate_inline (one heap)
  After:  1. inline-only bmalloc_heap free (small-segregated, TLC present, non-MTE) -- unchanged fast path
          else freeOutOfLine:
          2. bmalloc_try_deallocate_casual
          3. tagged_bmalloc_try_deallocate_inline_only
          4. tagged_bmalloc_deallocate_casual

In MTE-enabled processes, non-compact allocations now come from a separate tagged heap, which changes how they are tagged, freed and laid out, and with them the heap-grooming and MTE-bypass assumptions built on that layout. The default heap's inline fastMalloc shrinks from 141 to 73 instructions because tag-setting moves out of line, while fastFree grows from 44 to 45. The non-MTE WebContent fast path is deliberately left essentially untouched.

This is a very large allocator rewrite touching the core allocate, free, realloc and large-heap paths, and it changes invariants that exploit mitigations rely on.