Untitled

#pcplost#cross-cache

PCPLOST is a technique used to create cross-cache attacks. In simple terms, it allows a vulnerability involving one type of kernel object to corrupt a completely different type of kernel object.

Normally, the Linux SLUB allocator keeps different types of objects in separate kmem_caches. For example, suppose a vulnerable object belongs to cache A and the object an attacker wants to corrupt belongs to cache B. Even if the vulnerable object has an out-of-bounds (OOB) write bug, the memory used by cache B is normally unrelated to the memory used by cache A. As a result, writing past the end of an object in cache A will not normally reach an object from cache B. PCPLOST helps in this situation by manipulating the memory allocator underneath SLUB: the Buddy Allocator. The Buddy Allocator is responsible for providing physical pages to SLUB.

The goal is to arrange memory so that:

  1. SLUB obtains a page for objects from cache A.
  2. Another page, used for objects from cache B, is placed physically next to it.
  3. An OOB write from an object in cache A continues past the end of its page and into the physically adjacent page.
  4. That write can then reach and corrupt an object belonging to cache B.

1.

Before the technique makes sense, it helps to know the memory pipeline it’s exploiting:

Buddy Allocator (free_area lists, per-order)
        |
        v
   PCP lists (per-CPU page cache)
        |
        v
   SLUB caches (kmem_cache A, kmem_cache B, ...)
  • PCP lists (“per-CPU pageset”) are a small, fast, per-CPU cache of free pages, for allocation orders 0–3. Grabbing a page from here needs no lock and is very fast — this is the first place the kernel looks.
  • free_area is the shared, zone-wide buddy allocator, one free-list per order. When a PCP list runs dry, the kernel refills it from here. Refilling can trigger a page split: a large free block gets cut in half, producing two pages that are physically contiguous (called the L-buddy and R-buddy).
  • The attacker’s leverage is exactly that split: if you can force it to happen at a moment you control, and then steer one cache onto the L-buddy and the other cache onto the R-buddy, the two caches end up adjacent — by construction, not luck.

The problem: userspace can’t directly see which of these three paths (SLUB in-cache reuse / PCP hit / free_area refill-with-split) an allocation actually took. PCPLOST solves this with a timing side channel:

  • Timing side-channel: measure syscall latency (using disposable “timing objects” plus rdtsc) to infer which path an allocation used. Roughly: fast (SLUB in-cache) < PCP list hit < free_area/rmqueue_bulk() (slowest, since it may need to split a page).
  • Probe-and-drain loop: send a cheap “probe” (a timing object) → read the timing to infer the allocator’s current state → if needed, “drain” (allocate persistent objects to empty out a list) → repeat, until the desired allocator event (e.g. a split) is observed.
  • Because timing is noisy, a single fast/slow measurement isn’t trusted on its own. The attack re-tests for recurrence — sprays again to refill the “expected” area and re-measures — trading a bit of extra memory pressure for much higher confidence that the inferred state is real.

3. Two massaging strategies

Once you can detect a page split via timing, there are two ways to use it, depending on whether the vulnerable and target objects are the same allocation size class (“order”) or not.

Case Idea
Same-order (nv = nt) Drain the vulnerable cache’s PCP list until a split-refill occurs. This produces two new, physically-contiguous pages. Grab the first for the vulnerable cache (its object ends up placed last in that page’s slab, i.e. right at the boundary) and the second for the target cache (its object ends up placed first, also right at the boundary). Reported ~90%+ reliable.
Cross-order (nv ≠ nt) Exploits an implementation detail: when a batch refill happens, the last page pulled into the PCP list is often the L-buddy of a page that got split at a higher order — while its sibling, the R-buddy, is left sitting in free_area. So: drain until the L-buddy lands in the PCP list (the vulnerable object goes there), then get the target cache to independently pull the matching R-buddy straight out of free_area. This works reliably when nv > nt. When nv < nt by more than one order it’s unreliable, because the attacker can no longer tell which order the detected split happened at — meaning the real vulnerability may need to be guessed-and-retriggered blind, risking a crash.

4. Pivoting: extending to temporal bugs (UAF / double-free)

PCPLOST as described above is naturally suited to spatial bugs — an out-of-bounds read/write. Most real bugs found in the wild, though, are temporal (use-after-free, double-free). To bridge that gap, PCPLOST introduces a pivot object: any struct that has a length/size field which later feeds into a memcpy-like copy routine (the paper’s example is struct msg_msg). The idea is to turn a temporal bug into a spatial one, and then hand it off to the adjacency machinery already described above.

  • UAF → OOB: free the vulnerable object but keep the dangling pointer → reallocate a pivot object into that same freed slot → use the dangling pointer to corrupt the pivot object’s length field → trigger the pivot’s own copy routine, which now reads a corrupted (too-large) length → this produces a genuine out-of-bounds write. Because the vulnerable cache’s adjacency to the target cache was already pre-arranged (using ordinary PCPLOST from §2–3), this OOB write lands squarely in the target cache.
  • Double-free → OOB: first convert the double-free into a UAF (via the standard double-alloc/double-free trick), then apply the UAF → OOB pivot above.

5. ★ Bypassing SLAB_VIRTUAL ★

What SLAB_VIRTUAL actually guarantees:

Once a virtual address range is bound to one kmem_cache, it is never reassigned to a different kmem_cache.

  • This stops the classic form of cross-cache attack (SLUBStick-style page recycling): an attacker frees a page in cache A, hoping the allocator hands that exact same physical page to cache B next. Under SLAB_VIRTUAL that reassignment can never happen.
  • It does not unmap page-table entries, and it does not prevent reuse within the same cache — freeing a slot and getting a new object of the same cache back into that slot is completely untouched by the mitigation.
  • Crucially, it also says nothing about whether two different caches’ permanently-separate virtual-address ranges happen to sit physically or virtually next to each other. Adjacency is simply outside what the guarantee covers.

Why PCPLOST still works — by pivoting entirely in-cache:

  1. The UAF happens inside the vulnerable cache only: free the vulnerable object, then reallocate the pivot object into that same slot, same cache. No virtual address is ever reassigned between two different caches, so SLAB_VIRTUAL’s invariant is never triggered, let alone violated.
  2. Corrupt the pivot object’s length field through the dangling pointer, triggering its OOB write. This is now a purely spatial overflow — the temporal (UAF) part of the bug is already “spent” and out of the picture.
  3. That OOB write spills into whatever is physically adjacent to the vulnerable cache’s page — which is the target cache’s slab, because ordinary PCPLOST page-adjacency massaging (§2–3) was already used to arrange that ahead of time.
  4. SLAB_VIRTUAL never claimed to prevent adjacency between caches — only reassignment of a VA range across caches. An adjacency-based OOB write walks straight past it.
  5. Net result: in-cache pivoting still works under SLAB_VIRTUAL; only cross-cache pivoting is blocked (since that would require reassigning a VA to a different cache, which is exactly the thing SLAB_VIRTUAL exists to prevent).

Bonus for the attacker: SLAB_VIRTUAL actually makes slab placement more predictable, since it gives each cache its own virtually-contiguous private region. In some configurations this makes PCPLOST’s adjacency success rate even higher than on a vanilla kernel.

Result: the paper reports >90% success achieving a favorable OOB layout under SLAB_VIRTUAL, for both same-order and cross-order massaging, with a working pivoting path for UAF/double-free bugs.

6. ★ SLAB_VIRTUAL_GP (Guard Pages) ★

  • SLAB_VIRTUAL’s original public LKML patch has no guard pages — it’s just the VA-reassignment-prevention guarantee described in §5.
  • A newer version, deployed in Google’s kernelCTF, adds guard pages: a reserved virtual page (unmapped, no physical memory backing it) placed on either side of each virtual slab region.
  • The paper calls this variant SLAB_VIRTUAL_GP.

What guard pages actually fix:

  • They block linear overflows — the straightforward “walk off the end of the buffer” OOB write that PCPLOST relies on. Instead of silently landing in the neighboring (target) cache’s page, the write now hits an unmapped guard page and immediately faults/crashes.
  • This is directly effective against the exact adjacency-based attack PCPLOST performs, for linear OOB primitives specifically.

What it does NOT fix — the residual attack surface:

  • Non-linear overflows can still get through. A non-linear primitive is a “write N controlled bytes starting at offset K past the vulnerable object” — not a strict, sequential walk off the end.
    • If the attacker’s primitive can write beyond distance_to_slab_end + PAGE_SIZE — i.e. far enough to clear the entire guard page and land past it — it can still reach into the next real slab beyond the guard page.
    • This requires a bigger, offsettable overflow than the basic linear case, so it’s more constrained — but it is not eliminated by guard pages alone.
  • In short: guard pages raise the bar (a stronger primitive is now required) but leave a residual gap for sufficiently powerful, offset-controllable overflow bugs.

Performance cost (measured via LMBench, Table XI):

  • Roughly ~0.9% geomean overhead on latency benchmarks.
  • Roughly ~3.6% geomean regression on bandwidth benchmarks (some individual benchmarks, like read/read open2close, show up to ~15%).
  • Not free, but broadly modest — the real obstacle to this being merged upstream is less about this overhead and more about it not being mainlined yet, plus prior Torvalds/Molnár concerns about the base SLAB_VIRTUAL design (performance, and lack of DMA support).

References