Commit c96585e7bff for php.net
commit c96585e7bff6b8819b1888b0482bbe5875c5a4f6
Author: Julien Voisin <jvoisin@users.noreply.github.com>
Date: Fri Sep 4 14:17:26 2026 +0200
Protect the cached chunk list against corruption (#23367)
Empty chunks are not always unmapped. zend_mm_delete_chunk() may retain
them in heap->cached_chunks so they can be reused without another mmap().
The list is linked through chunk headers that stay mapped and writable, so
an overwrite of a link controls the value that zend_mm_alloc_pages()
removes from the cache and hands to zend_mm_chunk_init(), which writes
through it and links it into the active chunk list.
Protect the list with the same key material as the small allocation
freelists. chunk->next keeps the plain pointer and the new
chunk->next_shadow holds an encoded copy:
next_shadow = BSWAPPTR(next) ^ heap->shadow_key ^ &chunk->next_shadow
The byte swap makes a small overwrite corrupt the most significant bytes
of the address, which is unlikely to yield another valid chunk. Mixing in
the address of next_shadow prevents a valid (next, next_shadow) pair from
being replayed into another chunk. The shadow is an integrity check, not a
secret; the secret remains heap->shadow_key.
Reading a cached link decodes the shadow, checks that the result is
chunk-aligned and that it matches chunk->next, and only then dereferences
it. The head of the list is stored in the heap rather than in a chunk
header, so it gets an alignment check of its own when it is popped.
Cached chunks outlive request resets and forks, so their shadows are
recomputed by zend_mm_rekey_cached_chunks() whenever zend_mm_refresh_key()
or zend_mm_refresh_key_child() changes the key. That walk validates every
link against its old shadow, so corruption is detected rather than
silently re-encoded.
The next_shadow field is carved out of the chunk header's reserve field,
so the header is still 64 bytes, and chunk->next remains the ordinary
doubly-linked-list pointer while the chunk is active.
diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
index 09b29182f9e..f6c0a1ad0e9 100644
--- a/Zend/zend_alloc.c
+++ b/Zend/zend_alloc.c
@@ -326,10 +326,11 @@ struct _zend_mm_chunk {
zend_mm_heap *heap;
zend_mm_chunk *next;
zend_mm_chunk *prev;
+ zend_mm_chunk *next_shadow; /* shadow of "next" while the chunk is cached */
uint32_t free_pages; /* number of free pages */
uint32_t free_tail; /* number of free pages at the end of chunk */
uint32_t num;
- char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)];
+ char reserve[64 - (sizeof(void*) * 4 + sizeof(uint32_t) * 3)];
zend_mm_heap heap_slot; /* used only in main chunk */
zend_mm_page_map free_map; /* 512 bits or 64 bytes */
zend_mm_page_info map[ZEND_MM_PAGES]; /* 2 KB = 512 * 4 */
@@ -883,6 +884,64 @@ static zend_always_inline void zend_mm_chunk_init(zend_mm_heap *heap, zend_mm_ch
chunk->map[0] = ZEND_MM_LRUN(ZEND_MM_FIRST_PAGE);
}
+/* Cached chunks are linked through their headers, which live in memory a heap
+ * overflow can reach, so the link is mirrored in an encoded shadow. The shadow
+ * is byte-swapped, so that small overwrites hit the most significant bytes of
+ * the address, XOR'ed with the heap key, and XOR'ed with its own address so
+ * that a valid (link, shadow) pair cannot be replayed into another chunk. */
+static zend_always_inline zend_mm_chunk *zend_mm_encode_cached_chunk(const zend_mm_heap *heap, const void *holder, const zend_mm_chunk *next)
+{
+#ifdef WORDS_BIGENDIAN
+ return (zend_mm_chunk*)((uintptr_t)next ^ heap->shadow_key ^ (uintptr_t)holder);
+#else
+ return (zend_mm_chunk*)(BSWAPPTR((uintptr_t)next) ^ heap->shadow_key ^ (uintptr_t)holder);
+#endif
+}
+
+static zend_always_inline zend_mm_chunk *zend_mm_decode_cached_chunk_key(uintptr_t key, const void *holder, const zend_mm_chunk *encoded)
+{
+#ifdef WORDS_BIGENDIAN
+ zend_mm_chunk *next = (zend_mm_chunk*)((uintptr_t)encoded ^ key ^ (uintptr_t)holder);
+#else
+ zend_mm_chunk *next = (zend_mm_chunk*)(BSWAPPTR((uintptr_t)encoded ^ key ^ (uintptr_t)holder));
+#endif
+
+ ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(next, ZEND_MM_CHUNK_SIZE) == 0, "zend_mm_heap corrupted");
+ return next;
+}
+
+static zend_always_inline void zend_mm_set_next_cached_chunk(zend_mm_heap *heap, zend_mm_chunk *chunk, zend_mm_chunk *next)
+{
+ chunk->next = next;
+ chunk->next_shadow = zend_mm_encode_cached_chunk(heap, &chunk->next_shadow, next);
+}
+
+static zend_always_inline zend_mm_chunk *zend_mm_get_next_cached_chunk_key(uintptr_t key, const zend_mm_chunk *chunk)
+{
+ zend_mm_chunk *next = zend_mm_decode_cached_chunk_key(key, &chunk->next_shadow, chunk->next_shadow);
+
+ ZEND_MM_CHECK(chunk->next == next, "zend_mm_heap corrupted");
+ return next;
+}
+
+static zend_always_inline zend_mm_chunk *zend_mm_get_next_cached_chunk(const zend_mm_heap *heap, const zend_mm_chunk *chunk)
+{
+ return zend_mm_get_next_cached_chunk_key(heap->shadow_key, chunk);
+}
+
+/* Re-encode the cached links after the heap key changed. */
+static zend_always_inline void zend_mm_rekey_cached_chunks(zend_mm_heap *heap, uintptr_t old_key)
+{
+ zend_mm_chunk *chunk = heap->cached_chunks;
+
+ while (chunk != NULL) {
+ zend_mm_chunk *next = zend_mm_get_next_cached_chunk_key(old_key, chunk);
+
+ zend_mm_set_next_cached_chunk(heap, chunk, next);
+ chunk = next;
+ }
+}
+
/***********************/
/* Huge Runs (forward) */
/***********************/
@@ -1031,7 +1090,9 @@ static void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count ZEND_F
if (heap->cached_chunks) {
heap->cached_chunks_count--;
chunk = heap->cached_chunks;
- heap->cached_chunks = chunk->next;
+ /* The list head lives in the heap, which is as reachable as the chunk headers. */
+ ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(chunk, ZEND_MM_CHUNK_SIZE) == 0, "zend_mm_heap corrupted");
+ heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, chunk);
} else {
#if ZEND_MM_LIMIT
if (UNEXPECTED(ZEND_MM_CHUNK_SIZE > heap->limit - heap->real_size)) {
@@ -1150,7 +1211,7 @@ static zend_always_inline void zend_mm_delete_chunk(zend_mm_heap *heap, zend_mm_
&& heap->last_chunks_delete_count >= 4)) {
/* delay deletion */
heap->cached_chunks_count++;
- chunk->next = heap->cached_chunks;
+ zend_mm_set_next_cached_chunk(heap, chunk, heap->cached_chunks);
heap->cached_chunks = chunk;
} else {
#if ZEND_MM_STAT || ZEND_MM_LIMIT
@@ -1168,7 +1229,7 @@ static zend_always_inline void zend_mm_delete_chunk(zend_mm_heap *heap, zend_mm_
zend_mm_chunk_free(heap, chunk, ZEND_MM_CHUNK_SIZE);
} else {
//TODO: select the best chunk to delete???
- chunk->next = heap->cached_chunks->next;
+ zend_mm_set_next_cached_chunk(heap, chunk, zend_mm_get_next_cached_chunk(heap, heap->cached_chunks));
zend_mm_chunk_free(heap, heap->cached_chunks, ZEND_MM_CHUNK_SIZE);
heap->cached_chunks = chunk;
}
@@ -2068,6 +2129,8 @@ ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
}
}
+ zend_mm_rekey_cached_chunks(heap, old_key);
+
#if ZEND_DEBUG
heap->pid = getpid();
#endif
@@ -2521,7 +2584,7 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
p = heap->main_chunk->next;
while (p != heap->main_chunk) {
zend_mm_chunk *q = p->next;
- p->next = heap->cached_chunks;
+ zend_mm_set_next_cached_chunk(heap, p, heap->cached_chunks);
heap->cached_chunks = p;
p = q;
heap->chunks_count--;
@@ -2532,7 +2595,7 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
/* free all cached chunks */
while (heap->cached_chunks) {
p = heap->cached_chunks;
- heap->cached_chunks = p->next;
+ heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, p);
zend_mm_chunk_free(heap, p, ZEND_MM_CHUNK_SIZE);
}
/* free the first chunk */
@@ -2543,16 +2606,16 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
while ((double)heap->cached_chunks_count + 0.9 > heap->avg_chunks_count &&
heap->cached_chunks) {
p = heap->cached_chunks;
- heap->cached_chunks = p->next;
+ heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, p);
zend_mm_chunk_free(heap, p, ZEND_MM_CHUNK_SIZE);
heap->cached_chunks_count--;
}
/* clear cached chunks */
p = heap->cached_chunks;
while (p != NULL) {
- zend_mm_chunk *q = p->next;
+ zend_mm_chunk *q = zend_mm_get_next_cached_chunk(heap, p);
memset(p, 0, sizeof(zend_mm_chunk));
- p->next = q;
+ zend_mm_set_next_cached_chunk(heap, p, q);
p = q;
}
@@ -2589,7 +2652,12 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
&& "heap was re-used without calling zend_mm_refresh_key_child() after a fork");
#endif
+ uintptr_t old_key = heap->shadow_key;
+
zend_mm_refresh_key(heap);
+
+ /* Cached chunks outlive the request, so re-encode their links */
+ zend_mm_rekey_cached_chunks(heap, old_key);
}
}
@@ -2936,7 +3004,7 @@ ZEND_API zend_result zend_set_memory_limit(size_t memory_limit)
/* free some cached chunks to fit into new memory limit */
do {
zend_mm_chunk *p = heap->cached_chunks;
- heap->cached_chunks = p->next;
+ heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, p);
zend_mm_chunk_free(heap, p, ZEND_MM_CHUNK_SIZE);
heap->cached_chunks_count--;
heap->real_size -= ZEND_MM_CHUNK_SIZE;