PCPLost - PCP Massaging

#pcplost#cross-cache
Vuln Classesoobuafdouble-free
Requiresoob-or-uaf-or-double-free-primitivetiming-side-channel-measurement
Givescross-cache-page-adjacencyheap-groomingslab-virtual-bypass

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. Detecting the page split

PCPLOST relies on the PCP and buddy allocator behavior described in Buddy Allocator:

  • The attacker’s leverage is a page 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.
  • Userspace can’t directly see which allocation path was taken. PCPLOST solves this with a timing side channel:

Timing side channel: PCPLOST measures the latency of an allocation-related syscall using rdtsc to infer which allocator path was likely taken. Different paths have different costs: reusing memory already available to SLUB is generally the fastest, obtaining a page from the local PCP list is slower, and falling back to the zone-wide free_area through rmqueue_bulk() is typically the slowest. The last path may also involve refilling the PCP list and splitting a larger Buddy Allocator block, making it particularly interesting to the attack.

    SLUB in-cache reuse  →  PCP hit  →  free_area / rmqueue_bulk()
         fastest             slower          slowest
  • Probe-and-drain loop: The attacker repeatedly uses a small, disposable allocation as a probe. Its allocation time provides an indirect indication of the allocator’s current state. If the timing suggests that the desired path has not yet been reached, the attacker performs a drain by allocating persistent objects that consume pages from the relevant allocator layer. The attacker then probes again. By repeatedly probing, measuring, and draining, the allocator can be pushed toward the desired event, such as a refill that causes a Buddy Allocator split.

  • Re-testing for recurrence: Timing measurements are noisy and cannot reliably prove that a particular allocator event occurred from a single observation. PCPLOST therefore does not immediately trust one unusually fast or slow allocation. Instead, after observing the expected timing pattern, it performs additional sprays and probes to see whether the same pattern reappears under the expected allocator conditions. This consumes additional memory and creates more allocator pressure, but significantly increases confidence that the inferred allocator state is valid.


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 that means the real vulnerability may need to be guessed-and-retriggered blind, risking a crash. Therefore, in this latter case, the paper advises the attacker to choose target object size lesser than victim object size.

4. 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 PCPLOST), this OOB write lands 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. SLAB_VIRTUAL

PCPLOST can still work under SLAB_VIRTUAL by keeping the entire UAF pivot within the same SLUB cache. The vulnerable object is freed and the pivot object is reallocated into the same slot, so no virtual address is reassigned between different caches and SLAB_VIRTUAL’s invariant is never violated. The dangling pointer is then used to corrupt the pivot’s length field, turning the original UAF into a spatial OOB write.

Because PCPLOST has already arranged for the vulnerable cache’s slab to be physically adjacent to the target cache’s slab, the OOB write can cross the page boundary and corrupt the target object. SLAB_VIRTUAL prevents virtual-address reassignment between caches, but it does not prevent physical adjacency or an OOB write from crossing into an adjacent slab.

Therefore, SLAB_VIRTUAL blocks cross-cache pivoting, but it does not prevent in-cache pivoting followed by cross-cache OOB corruption. In fact, by giving each cache a predictable virtually contiguous region, SLAB_VIRTUAL can sometimes make slab placement more predictable and improve PCPLOST’s adjacency success rate.


6. SLAB_VIRTUAL_GP

Sadly, 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 to prevent exactly this kind of Out of Bound read/writes.


References