Skip to content

Commit 56ede4d

Browse files
committed
Protect the cached chunk list against corruption
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.
1 parent 9f1437e commit 56ede4d

1 file changed

Lines changed: 78 additions & 10 deletions

File tree

Zend/zend_alloc.c

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,11 @@ struct _zend_mm_chunk {
326326
zend_mm_heap *heap;
327327
zend_mm_chunk *next;
328328
zend_mm_chunk *prev;
329+
zend_mm_chunk *next_shadow; /* shadow of "next" while the chunk is cached */
329330
uint32_t free_pages; /* number of free pages */
330331
uint32_t free_tail; /* number of free pages at the end of chunk */
331332
uint32_t num;
332-
char reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)];
333+
char reserve[64 - (sizeof(void*) * 4 + sizeof(uint32_t) * 3)];
333334
zend_mm_heap heap_slot; /* used only in main chunk */
334335
zend_mm_page_map free_map; /* 512 bits or 64 bytes */
335336
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
883884
chunk->map[0] = ZEND_MM_LRUN(ZEND_MM_FIRST_PAGE);
884885
}
885886

887+
/* Cached chunks are linked through their headers, which live in memory a heap
888+
* overflow can reach, so the link is mirrored in an encoded shadow. The shadow
889+
* is byte-swapped, so that small overwrites hit the most significant bytes of
890+
* the address, XOR'ed with the heap key, and XOR'ed with its own address so
891+
* that a valid (link, shadow) pair cannot be replayed into another chunk. */
892+
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)
893+
{
894+
#ifdef WORDS_BIGENDIAN
895+
return (zend_mm_chunk*)((uintptr_t)next ^ heap->shadow_key ^ (uintptr_t)holder);
896+
#else
897+
return (zend_mm_chunk*)(BSWAPPTR((uintptr_t)next) ^ heap->shadow_key ^ (uintptr_t)holder);
898+
#endif
899+
}
900+
901+
static zend_always_inline zend_mm_chunk *zend_mm_decode_cached_chunk_key(uintptr_t key, const void *holder, const zend_mm_chunk *encoded)
902+
{
903+
#ifdef WORDS_BIGENDIAN
904+
zend_mm_chunk *next = (zend_mm_chunk*)((uintptr_t)encoded ^ key ^ (uintptr_t)holder);
905+
#else
906+
zend_mm_chunk *next = (zend_mm_chunk*)(BSWAPPTR((uintptr_t)encoded ^ key ^ (uintptr_t)holder));
907+
#endif
908+
909+
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(next, ZEND_MM_CHUNK_SIZE) == 0, "zend_mm_heap corrupted");
910+
return next;
911+
}
912+
913+
static zend_always_inline void zend_mm_set_next_cached_chunk(zend_mm_heap *heap, zend_mm_chunk *chunk, zend_mm_chunk *next)
914+
{
915+
chunk->next = next;
916+
chunk->next_shadow = zend_mm_encode_cached_chunk(heap, &chunk->next_shadow, next);
917+
}
918+
919+
static zend_always_inline zend_mm_chunk *zend_mm_get_next_cached_chunk_key(uintptr_t key, const zend_mm_chunk *chunk)
920+
{
921+
zend_mm_chunk *next = zend_mm_decode_cached_chunk_key(key, &chunk->next_shadow, chunk->next_shadow);
922+
923+
ZEND_MM_CHECK(chunk->next == next, "zend_mm_heap corrupted");
924+
return next;
925+
}
926+
927+
static zend_always_inline zend_mm_chunk *zend_mm_get_next_cached_chunk(const zend_mm_heap *heap, const zend_mm_chunk *chunk)
928+
{
929+
return zend_mm_get_next_cached_chunk_key(heap->shadow_key, chunk);
930+
}
931+
932+
/* Re-encode the cached links after the heap key changed. */
933+
static zend_always_inline void zend_mm_rekey_cached_chunks(zend_mm_heap *heap, uintptr_t old_key)
934+
{
935+
zend_mm_chunk *chunk = heap->cached_chunks;
936+
937+
while (chunk != NULL) {
938+
zend_mm_chunk *next = zend_mm_get_next_cached_chunk_key(old_key, chunk);
939+
940+
zend_mm_set_next_cached_chunk(heap, chunk, next);
941+
chunk = next;
942+
}
943+
}
944+
886945
/***********************/
887946
/* Huge Runs (forward) */
888947
/***********************/
@@ -1031,7 +1090,9 @@ static void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count ZEND_F
10311090
if (heap->cached_chunks) {
10321091
heap->cached_chunks_count--;
10331092
chunk = heap->cached_chunks;
1034-
heap->cached_chunks = chunk->next;
1093+
/* The list head lives in the heap, which is as reachable as the chunk headers. */
1094+
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(chunk, ZEND_MM_CHUNK_SIZE) == 0, "zend_mm_heap corrupted");
1095+
heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, chunk);
10351096
} else {
10361097
#if ZEND_MM_LIMIT
10371098
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_
11501211
&& heap->last_chunks_delete_count >= 4)) {
11511212
/* delay deletion */
11521213
heap->cached_chunks_count++;
1153-
chunk->next = heap->cached_chunks;
1214+
zend_mm_set_next_cached_chunk(heap, chunk, heap->cached_chunks);
11541215
heap->cached_chunks = chunk;
11551216
} else {
11561217
#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_
11681229
zend_mm_chunk_free(heap, chunk, ZEND_MM_CHUNK_SIZE);
11691230
} else {
11701231
//TODO: select the best chunk to delete???
1171-
chunk->next = heap->cached_chunks->next;
1232+
zend_mm_set_next_cached_chunk(heap, chunk, zend_mm_get_next_cached_chunk(heap, heap->cached_chunks));
11721233
zend_mm_chunk_free(heap, heap->cached_chunks, ZEND_MM_CHUNK_SIZE);
11731234
heap->cached_chunks = chunk;
11741235
}
@@ -2037,6 +2098,8 @@ ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap)
20372098
}
20382099
}
20392100

2101+
zend_mm_rekey_cached_chunks(heap, old_key);
2102+
20402103
#if ZEND_DEBUG
20412104
heap->pid = getpid();
20422105
#endif
@@ -2489,7 +2552,7 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
24892552
p = heap->main_chunk->next;
24902553
while (p != heap->main_chunk) {
24912554
zend_mm_chunk *q = p->next;
2492-
p->next = heap->cached_chunks;
2555+
zend_mm_set_next_cached_chunk(heap, p, heap->cached_chunks);
24932556
heap->cached_chunks = p;
24942557
p = q;
24952558
heap->chunks_count--;
@@ -2500,7 +2563,7 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
25002563
/* free all cached chunks */
25012564
while (heap->cached_chunks) {
25022565
p = heap->cached_chunks;
2503-
heap->cached_chunks = p->next;
2566+
heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, p);
25042567
zend_mm_chunk_free(heap, p, ZEND_MM_CHUNK_SIZE);
25052568
}
25062569
/* free the first chunk */
@@ -2511,16 +2574,16 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
25112574
while ((double)heap->cached_chunks_count + 0.9 > heap->avg_chunks_count &&
25122575
heap->cached_chunks) {
25132576
p = heap->cached_chunks;
2514-
heap->cached_chunks = p->next;
2577+
heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, p);
25152578
zend_mm_chunk_free(heap, p, ZEND_MM_CHUNK_SIZE);
25162579
heap->cached_chunks_count--;
25172580
}
25182581
/* clear cached chunks */
25192582
p = heap->cached_chunks;
25202583
while (p != NULL) {
2521-
zend_mm_chunk *q = p->next;
2584+
zend_mm_chunk *q = zend_mm_get_next_cached_chunk(heap, p);
25222585
memset(p, 0, sizeof(zend_mm_chunk));
2523-
p->next = q;
2586+
zend_mm_set_next_cached_chunk(heap, p, q);
25242587
p = q;
25252588
}
25262589

@@ -2557,7 +2620,12 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
25572620
&& "heap was re-used without calling zend_mm_refresh_key_child() after a fork");
25582621
#endif
25592622

2623+
uintptr_t old_key = heap->shadow_key;
2624+
25602625
zend_mm_refresh_key(heap);
2626+
2627+
/* Cached chunks outlive the request, so re-encode their links */
2628+
zend_mm_rekey_cached_chunks(heap, old_key);
25612629
}
25622630
}
25632631

@@ -2904,7 +2972,7 @@ ZEND_API zend_result zend_set_memory_limit(size_t memory_limit)
29042972
/* free some cached chunks to fit into new memory limit */
29052973
do {
29062974
zend_mm_chunk *p = heap->cached_chunks;
2907-
heap->cached_chunks = p->next;
2975+
heap->cached_chunks = zend_mm_get_next_cached_chunk(heap, p);
29082976
zend_mm_chunk_free(heap, p, ZEND_MM_CHUNK_SIZE);
29092977
heap->cached_chunks_count--;
29102978
heap->real_size -= ZEND_MM_CHUNK_SIZE;

0 commit comments

Comments
 (0)