Commit 946c799de44 for php.net
commit 946c799de44e454f5138dfaa3704cf4d931fb247
Author: Julien Voisin <jvoisin@users.noreply.github.com>
Date: Fri Sep 4 14:00:44 2026 +0200
Bind free list shadow pointers to their storage location (#23376)
The shadow of a free list pointer is currently BSWAP(next) ^ shadow_key,
which does not depend on where it is stored, meaning that:
- zend_mm_free_small() encodes whatever heap->free_slot[bin] happens to
be, including NULL when the bin has been drained, meaning
the slot ends up holding shadow_key verbatim.
- A (next, shadow) pair harvested from one free slot is valid in every
other slot of every bin. An attacker who can read one free slot can
therefore forge a link anywhere in the heap without needing the key.
This commit adds the address of the shadow itself into the mix:
shadow = BSWAP(next) ^ shadow_key ^ (uintptr_t)&shadow
The holder term cancels on decode, so this is one extra xor on a register
that is already live, with no branch. Encoding NULL now yields
shadow_key ^ holder rather than the key, and a shadow only verifies in the
slot it was written for.
Using the address of the shadow rather than the address of the slot means
that zend_mm_get_next_free_slot() does not need to keep the slot alive
after fetching next, which keeps the register pressure unchanged.
This was verified under GDB: Freeing into a drained bin used to store shadow_key
exactly; it now stores shadow_key ^ holder (xoring the two back gives the
address of the shadow). Naïvely replaying a valid (next, shadow) pair from one
slot into another and traversing from it is accepted before this change and
aborts with "zend_mm_heap corrupted" after.
Performance-wise, the impact is in the noise level, which is expected as it more
or less adds a single `xor` instruction per `zend_mm_set_next_free_slot()`.
This commit is a follow up on 25360ef2495 and c561f7da858.
Co-authored-by: Arnaud Le Blanc <arnaud.lb@gmail.com>
diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
index 97cd2f895d3..09b29182f9e 100644
--- a/Zend/zend_alloc.c
+++ b/Zend/zend_alloc.c
@@ -1276,35 +1276,38 @@ static zend_always_inline int zend_mm_small_size_to_bin(size_t size)
* before dereference by comparing them with a shadow.
*
* The shadow is a copy of the pointer, stored at the end of the slot. It is
- * XOR'ed with a random key, and converted to big-endian so that smaller
- * corruptions affect the most significant bytes, which has a high chance of
- * resulting in an invalid address instead of pointing to an adjacent slot.
+ * XOR'ed with a random key and with its own address, and converted to
+ * big-endian so that smaller corruptions affect the most significant bytes,
+ * which has a high chance of resulting in an invalid address instead of
+ * pointing to an adjacent slot. Mixing in the holder address keeps the key from
+ * being stored verbatim when the encoded pointer is NULL, and prevents a valid
+ * shadow from being naïvely replayed into another slot.
*/
-#define ZEND_MM_FREE_SLOT_PTR_SHADOW(free_slot, bin_num) \
- *((zend_mm_free_slot**)((char*)(free_slot) + bin_data_size[(bin_num)] - sizeof(zend_mm_free_slot*)))
+#define ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(free_slot, bin_num) \
+ ((zend_mm_free_slot**)((char*)(free_slot) + bin_data_size[(bin_num)] - sizeof(zend_mm_free_slot*)))
-static zend_always_inline zend_mm_free_slot* zend_mm_encode_free_slot(const zend_mm_heap *heap, const zend_mm_free_slot *slot)
+static zend_always_inline zend_mm_free_slot* zend_mm_encode_free_slot(const zend_mm_heap *heap, const void *holder, const zend_mm_free_slot *next)
{
#ifdef WORDS_BIGENDIAN
- return (zend_mm_free_slot*)(((uintptr_t)slot) ^ heap->shadow_key);
+ return (zend_mm_free_slot*)((uintptr_t)next ^ heap->shadow_key ^ (uintptr_t)holder);
#else
- return (zend_mm_free_slot*)(BSWAPPTR((uintptr_t)slot) ^ heap->shadow_key);
+ return (zend_mm_free_slot*)(BSWAPPTR((uintptr_t)next) ^ heap->shadow_key ^ (uintptr_t)holder);
#endif
}
-static zend_always_inline zend_mm_free_slot* zend_mm_decode_free_slot_key(uintptr_t shadow_key, zend_mm_free_slot *slot)
+static zend_always_inline zend_mm_free_slot* zend_mm_decode_free_slot_key(uintptr_t shadow_key, const void *holder, zend_mm_free_slot *shadow)
{
#ifdef WORDS_BIGENDIAN
- return (zend_mm_free_slot*)((uintptr_t)slot ^ shadow_key);
+ return (zend_mm_free_slot*)((uintptr_t)shadow ^ shadow_key ^ (uintptr_t)holder);
#else
- return (zend_mm_free_slot*)(BSWAPPTR((uintptr_t)slot ^ shadow_key));
+ return (zend_mm_free_slot*)(BSWAPPTR((uintptr_t)shadow ^ shadow_key ^ (uintptr_t)holder));
#endif
}
-static zend_always_inline zend_mm_free_slot* zend_mm_decode_free_slot(zend_mm_heap *heap, zend_mm_free_slot *slot)
+static zend_always_inline zend_mm_free_slot* zend_mm_decode_free_slot(zend_mm_heap *heap, const void *holder, zend_mm_free_slot *shadow)
{
- return zend_mm_decode_free_slot_key(heap->shadow_key, slot);
+ return zend_mm_decode_free_slot_key(heap->shadow_key, holder, shadow);
}
static zend_always_inline void zend_mm_set_next_free_slot(zend_mm_heap *heap, uint32_t bin_num, zend_mm_free_slot *slot, zend_mm_free_slot *next)
@@ -1312,15 +1315,17 @@ static zend_always_inline void zend_mm_set_next_free_slot(zend_mm_heap *heap, ui
ZEND_ASSERT(bin_data_size[bin_num] >= ZEND_MM_MIN_USEABLE_BIN_SIZE);
slot->next_free_slot = next;
- ZEND_MM_FREE_SLOT_PTR_SHADOW(slot, bin_num) = zend_mm_encode_free_slot(heap, next);
+
+ zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, bin_num);
+ *shadow_addr = zend_mm_encode_free_slot(heap, shadow_addr, next);
}
static zend_always_inline zend_mm_free_slot *zend_mm_get_next_free_slot(zend_mm_heap *heap, uint32_t bin_num, zend_mm_free_slot* slot)
{
zend_mm_free_slot *next = slot->next_free_slot;
if (EXPECTED(next != NULL)) {
- zend_mm_free_slot *shadow = ZEND_MM_FREE_SLOT_PTR_SHADOW(slot, bin_num);
- if (UNEXPECTED(next != zend_mm_decode_free_slot(heap, shadow))) {
+ zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, bin_num);
+ if (UNEXPECTED(next != zend_mm_decode_free_slot(heap, shadow_addr, *shadow_addr))) {
zend_mm_panic("zend_mm_heap corrupted");
}
}
@@ -2054,8 +2059,8 @@ ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
}
zend_mm_free_slot *next;
while ((next = slot->next_free_slot)) {
- zend_mm_free_slot *shadow = ZEND_MM_FREE_SLOT_PTR_SHADOW(slot, i);
- if (UNEXPECTED(next != zend_mm_decode_free_slot_key(old_key, shadow))) {
+ zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, i);
+ if (UNEXPECTED(next != zend_mm_decode_free_slot_key(old_key, shadow_addr, *shadow_addr))) {
zend_mm_panic("zend_mm_heap corrupted");
}
zend_mm_set_next_free_slot(heap, i, slot, next);