Skip to content
  •  
  •  
  •  
60 changes: 60 additions & 0 deletions Zend/tests/partial_application/attributes_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
--TEST--
PFA attribute UAF
--CREDITS--
Ryan @ Calif.io
--FILE--
<?php

/* Reproduces with opcache.enable_cli=0. Not adding this to the INI section so
* that it's tested in all configurations. */

#[NoDiscard(ND_MESSAGE)]
function userTarget(mixed $value): mixed
{
return $value;
}

define('ND_MESSAGE', str_repeat('message-', 32).time());

function arguments(ReflectionFunction $function): array
{
return $function->getAttributes(NoDiscard::class)[0]->getArguments();
}

$original = new ReflectionFunction('userTarget');
var_dump(arguments($original));

$partial = userTarget(?);
echo "partial-created\n";
var_dump(arguments($original));
var_dump(arguments(new ReflectionFunction($partial)));

unset($partial);
gc_collect_cycles();
echo "partial-destroyed\n";
var_dump(arguments($original));

userTarget('discarded');

?>
--EXPECTF--
array(1) {
[0]=>
string(266) "message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-%d"
}
partial-created
array(1) {
[0]=>
string(266) "message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-%d"
}
array(1) {
[0]=>
string(266) "message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-%d"
}
partial-destroyed
array(1) {
[0]=>
string(266) "message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-%d"
}

Warning: The return value of function userTarget() should either be used or intentionally ignored by casting it as (void), message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-message-%d in %s on line %d
129 changes: 101 additions & 28 deletions Zend/zend_alloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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) */
/***********************/
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -1276,51 +1337,56 @@ 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)
{
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");
}
}
Expand Down Expand Up @@ -2054,15 +2120,17 @@ 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);
slot = next;
}
}

zend_mm_rekey_cached_chunks(heap, old_key);

#if ZEND_DEBUG
heap->pid = getpid();
#endif
Expand Down Expand Up @@ -2516,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--;
Expand All @@ -2527,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 */
Expand All @@ -2538,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;
}

Expand Down Expand Up @@ -2584,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);
}
}

Expand Down Expand Up @@ -2931,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;
Expand Down
5 changes: 4 additions & 1 deletion Zend/zend_partial.c
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,9 @@ static zend_ast *zp_attribute_to_ast(zend_attribute *attribute)
if (attribute->argc) {
args_ast = zend_ast_create_arg_list(0, ZEND_AST_ARG_LIST);
for (uint32_t i = 0; i < attribute->argc; i++) {
zend_ast *arg_ast = zend_ast_create_zval(&attribute->args[i].value);
zval *zv = &attribute->args[i].value;
Z_TRY_ADDREF_P(zv);
zend_ast *arg_ast = zend_ast_create_zval(zv);
if (attribute->args[i].name) {
arg_ast = zend_ast_create(ZEND_AST_NAMED_ARG,
zend_ast_create_zval_from_str(
Expand Down Expand Up @@ -561,6 +563,7 @@ static zend_ast *zp_compile_forwarding_call(
args_ast = zend_ast_list_add(args_ast, default_value_ast);
} else if (zp_is_const_arg(const_args, offset)) {
ZEND_ASSERT(Z_TYPE(argv[offset]) < IS_OBJECT);
ZEND_ASSERT(!Z_REFCOUNTED(argv[offset]));

/* This argument never changes, so we can burn it into the op_array
* and check its type ahead of time. */
Expand Down
4 changes: 2 additions & 2 deletions ext/opcache/shared_alloc_mmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ static int create_segments(size_t requested_size, zend_shared_segment ***shared_
/* to got HUGE PAGES in low 32-bit address we have to reserve address
space and then remap it using MAP_HUGETLB */

p = mmap(NULL, requested_size, flags, MAP_SHARED|MAP_ANONYMOUS|MAP_32BIT, fd, 0);
p = mmap(NULL, requested_size + huge_page_size, flags, MAP_SHARED|MAP_ANONYMOUS|MAP_32BIT, fd, 0);
if (p != MAP_FAILED) {
munmap(p, requested_size);
munmap(p, requested_size + huge_page_size);
p = (void*)(ZEND_MM_ALIGNED_SIZE_EX((ptrdiff_t)p, huge_page_size));
p = mmap(p, requested_size, flags, MAP_SHARED|MAP_ANONYMOUS|MAP_32BIT|MAP_HUGETLB|MAP_FIXED, -1, 0);
if (p != MAP_FAILED) {
Expand Down
2 changes: 1 addition & 1 deletion ext/openssl/tests/ServerClientTestCase.inc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function phpt_has_sslv3() {
if (!is_null($result)) {
return $result;
}
$server = @stream_socket_server('sslv3://127.0.0.1:10013');
$server = @stream_socket_server('sslv3://127.0.0.1:0');
if ($result = !!$server) {
fclose($server);
}
Expand Down
4 changes: 2 additions & 2 deletions ext/openssl/tests/gh20802.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ $serverCode = <<<'CODE'
]
]
]);
$server = stream_socket_server('tls://127.0.0.1:12443', $errno, $errstr, $flags, $ctx);
$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
phpt_notify_server_start($server);
stream_socket_accept($server, 3);
CODE;
Expand All @@ -42,7 +42,7 @@ $ctx = stream_context_create([
'verify_peer' => false
]
]);
@stream_socket_client("tls://127.0.0.1:12443", $errno, $errstr, 1, $flags, $ctx);
@stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 1, $flags, $ctx);
CODE;

include 'CertificateGenerator.inc';
Expand Down
4 changes: 2 additions & 2 deletions ext/openssl/tests/openssl_error_string_basic_openssl3.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function dump_openssl_errors($name) {
}

// common output file
$output_file = __DIR__ . "/openssl_error_string_basic_output.tmp";
$output_file = __DIR__ . "/openssl_error_string_basic_openssl3_output.tmp";
// invalid file for read is something that does not exist in current directory
$invalid_file_for_read = __DIR__ . "/invalid_file_for_read_operation.txt";
// invalid file for is the test dir as writing file to existing dir should always fail
Expand Down Expand Up @@ -156,7 +156,7 @@ expect_openssl_errors('openssl_csr_get_subjec pem', [$err_pem_no_start_line]);
?>
--CLEAN--
<?php
$output_file = __DIR__ . "/openssl_error_string_basic_output.tmp";
$output_file = __DIR__ . "/openssl_error_string_basic_openssl3_output.tmp";
if (is_file($output_file)) {
unlink($output_file);
}
Expand Down
Loading