snmalloc

repository·main·Indexed 23 days ago

https://github.com/microsoft/snmalloc

A high-performance memory allocator designed for extreme scalability in multi-threaded environments, particularly for cross-thread deallocations. It supports C++, Linux/BSD (via LD_PRELOAD), and Rust (via the snmalloc-rs crate v0.7.4). Key features include thread-local and remote deallocation using a lock-free message passing scheme, a layered architecture for address space management, and a hardened version providing allocation randomization, metadata protection, and corruption detection.

Tokens
13.4K
Snippets
19
Records
64
Agent score
83%

What's inside snmalloc

  1. What is a slab in snmalloc?

    main

    A slab is a naturally aligned, power-of-two sized chunk that has been partitioned into a series of allocations, where every allocation in that slab is exactly the same size.

    For example, a 16KiB slab can be split into 341 allocations of 48 bytes each, leaving 16 bytes unused at the end. This design allows snmalloc to support multiple slab sizes, ensuring that common allocation sizes can fit multiple times onto a single slab to minimize waste while reducing the frequency of the slower 'new slab' allocation path.

  2. What is Strict Provenance and how does snmalloc use it?

    main

    Strict Provenance refers to architectures (like CHERI/Arm Morello) that treat pointer authority (spatial bounds and virtual memory permissions) as a first-class citizen alongside the target address.

    snmalloc uses this concept to impose strong constraints on how clients use managed memory. By using bounded pointers, snmalloc ensures that a pointer returned from malloc() cannot be used to access adjacent allocations, preventing spatial overflows.

    Key concepts include:

    • Spatial Authority: The ability to read/write/execute within a specific interval.
    • vmem Authority: The ability to request modification of virtual page mappings.
    • Bounding: Deriving a new pointer with a subset of the original's authority.
    • Amplification: Constructing a pointer with increased authority (up to the original's limit).
  3. Constraints on Allocations and Deallocations

    main

    Allocation Constraints

    • No Overlap: snmalloc ensures no two live allocations have overlapping bounds. Pointers are bounded to no more than the slab entry used to back the allocation.
    • Vmem Stripping: Returned pointers are stripped of vmem authority (if supported) so clients cannot manipulate the underlying page mappings.
    • Realloc Policy: If realloc() changes the size class, it triggers an allocate-copy-deallocate sequence. Even if the object stays in place, the returned pointer is re-bounded as if it were a new allocation.

    Deallocation Behavior

    • Interior Pointers: Currently, snmalloc permits deallocating an object using a pointer to any part of that object. The allocator will find the object's start and end via metadata.
    • Lower Bound Lookup: During deallocation, snmalloc uses the lower bound address of the provided pointer to look up metadata, ensuring compatibility with architectures where bounds are monotonically non-increasing.
  4. How snmalloc prevents predictable slab exhaustion

    main

    To prevent attackers from easily predicting when a slab will be full (which could allow them to allocate objects adjacent to specific targets), snmalloc uses two mechanisms:

    1. Threshold-based reuse: A slab is only considered for reuse when it has a specific percentage of free elements.
    2. Probabilistic new slab allocation: If only a single slab is currently available for reuse, snmalloc uses a random coin flip to decide whether to continue using that existing slab or to allocate a brand new slab instead.
  5. Understand the snmalloc header hierarchy

    main

    snmalloc is organized into a layered hierarchy. Each layer provides abstractions that the layers above it depend on. Understanding this hierarchy helps in knowing where to find specific functionality or where to inject custom logic:

    • ds_core/: Core data structures (language extension abstractions).
    • aal/: Architecture Abstraction Layer (CPU intrinsics, virtual address-space size).
    • ds_aal/: Data structures depending on the AAL.
    • mitigations/: Security mitigation configuration (e.g., mitigations() function controlled by SNMALLOC_CHECK_CLIENT).
    • pal/: Platform Abstraction Layer (OS/environment abstractions).
    • ds/: Data structures depending on platform services or CPU features.
    • mem/: Core allocator abstractions (templated over a back-end).
    • backend_helpers/: Helpers for defining back ends (e.g., pagemap implementations, buddy allocators).
    • backend/: Example implementations of global memory allocators. Users can replace this with a custom back end.
    • global/: Front-end components assuming a global configuration.
    • override/: Implementations for specific language APIs (C malloc, C++ operator new, Rust std::alloc, etc.).
  6. How snmalloc provides guarded memcpy

    main

    snmalloc implements a 'guarded' version of memcpy to prevent out-of-bounds (OOB) heap corruption. When memcpy(dst, src, len) is called, snmalloc can verify that the operation does not cross the boundary of the allocated memory blocks.

    By default, for release builds, snmalloc performs a check to ensure the destination (dst) is large enough to hold len bytes. It can also be configured to check the source (src) to ensure it does not read beyond the end of the object.

    Safety Logic

    To ensure safety, the following checks are conceptually performed:

      if (src is managed by snmalloc)
        check(remaining_bytes(src) >= len);
      if (dst is managed by snmalloc)
        check(remaining_bytes(dst) >= len);

    Key Characteristics

    • Compatibility: If memory is not managed by snmalloc, the system assumes the operation is correct. This is achieved by mapping the 0 value in the chunk map to a virtual slab covering the entire address space.
    • Initialization Safety: The implementation includes a check to handle cases where memcpy is called before snmalloc has been initialized.
    • Performance: The overhead is most significant for very small copies (e.g., a single byte) but rapidly diminishes as the copy size increases. For copies of 128 bytes or more, the overhead is typically negligible.
  7. Hardened version of snmalloc

    main

    For security-sensitive applications, a hardened version of snmalloc is available. It provides the following protections:

    • Allocation Randomization: Randomizes the relative locations of allocations.
    • Metadata Protection: Most metadata is stored separately from allocations and is protected with guard pages.
    • Corruption Detection: All in-band metadata is protected with a novel encoding to detect corruption.
    • Safe memcpy: Provides a memcpy implementation that automatically checks bounds relative to the underlying malloc.
  8. How snmalloc uses randomisation for security

    main

    snmalloc employs randomisation of allocation patterns to increase the difficulty of memory corruption exploits. While not a complete defense against spraying, it raises the bar for attackers by making relative addresses harder to predict.

    Randomisation is applied to three specific areas:

    1. Initial order of allocations on a slab: Determines the starting sequence of objects.
    2. Subsequent order of allocations on a slab: Randomises the order of objects as they are reused.
    3. Slab consumption timing: Randomises when a slab is considered fully consumed/exhausted.
  9. Use CapPtr<T, B> for static pointer annotations

    main

    To aid code auditing and enforce constraints, snmalloc uses the CapPtr<T, B> wrapper type. The template parameter B acts as a static annotation characterizing the pointer's role or supported operations.

    Common roles for B include:

    • A pointer to a whole chunk or slab.
    • A pointer to a specific allocation destined for a user program.
    • A putative pointer received from a user program.

    Note: CapPtr is a tool for developer guidance and static auditing; on non-CHERI architectures, it does not provide runtime security against reinterpret_cast or unsafe pointer operations.

  10. How snmalloc protects free list meta-data

    main

    snmalloc uses a novel technique to protect "in-band" meta-data (data stored within the unused memory of allocations) from corruption via use-after-free or out-of-bounds writes.

    Instead of using a single linked list, snmalloc uses a doubly linked queue where both forward and backward pointers are encoded using cryptographic-like functions. This allows the allocator to verify the integrity of the list by checking the invariant: x.next.prev == x.

    Encoding Mechanism

    • Forward direction: Uses an involution f(a) = a XOR k0, where k0 is a randomly chosen value.
    • Backward direction: Uses a two-argument function g(a, b) = (a XOR k1) * (b XOR k2), where k1 and k2 are randomly chosen 64-bit values.

    Because the order of construction and consumption must match to maintain these invariants, snmalloc uses queues rather than stacks for its free lists.

    Benefits

    • Corruption Detection: Detects out-of-bounds writes or use-after-free attacks on the next or prev pointers during reallocation.
    • Double Free Protection: If an object is freed twice, it corrupts the prev pointer. The error is detected when the object is later reused.
    • Low Overhead: The protection is integrated into the fast path of allocation with minimal performance impact (one multiplication, one branch, and a few additional loads/stores).
  11. How the Combining Lock works

    main

    The Combining Lock is an optimization of the MCS queue lock designed to reduce contention. Instead of each thread only performing its own work, the thread that currently holds the lock (the 'head') can traverse the queue and execute the operations (lambdas) of other waiting threads.

    Core Abstractions

    • LockStatus: A state machine for each node in the queue:
      • WAITING: The thread is waiting for the lock to become available.
      • HEAD: The thread is responsible for completing more work from the queue.
      • DONE: Another thread has already completed the operation for this thread.
    • CombiningLockNode: A node in the queue containing a pointer to the next node, the current LockStatus, and a function pointer (f_raw) representing the work to be done.
    • CombiningLockNodeTempl<F>: A template wrapper that allows passing a lambda/function F and uses manual dispatch (via reinterpret_cast) instead of virtual tables to avoid vtable overhead.
    enum class LockStatus
    {
      WAITING,
      DONE,
      HEAD
    };
    
    struct CombiningLockNode
    {
      std::atomic<CombiningLockNode*> next{nullptr};
      std::atomic<LockStatus> status{WAITING};
      void (*f_raw)(CombiningLockNode*);
    
      void run()
      {
        f_raw(this);
      }
    };
    
    template<typename F>
    struct CombiningLockNodeTempl : CombiningLockNode
    {
      F f;
    
      CombiningLockNodeTempl(F&& f_)
      : CombiningLockNode([](CombiningLockNode* self) {
          auto self_templ = reinterpret_cast<CombiningLockNodeTempl*>(self);
          self_templ->f();
        }), 
        f(f_) {}
    };