Skip to content

Fix GH-14402: Add support for serialization in SplPriorityQueue, SplMinHeap and SplMaxHeap #19447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ PHP NEWS
opened directory has been deprecated. (Girgias)
. Fixed bug GH-19153 (#[\Attribute] validation should error on
trait/interface/enum/abstract class). (DanielEScherzer)
. Fixed GH-14402 (SplPriorityQueue, SplMinHeap, and SplMaxHeap lost their
data on serialize()). (alexandre-daubois)

- XML:
. The xml_parser_free() function has been deprecated. (DanielEScherzer)
Expand Down
215 changes: 215 additions & 0 deletions ext/spl/spl_heap.c
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,221 @@ PHP_METHOD(SplPriorityQueue, __debugInfo)
RETURN_ARR(spl_heap_object_get_debug_info(spl_ce_SplPriorityQueue, Z_OBJ_P(ZEND_THIS)));
} /* }}} */

static void spl_heap_serialize_properties(zval *return_value, spl_heap_object *intern)
{
HashTable *props = zend_std_get_properties(&intern->std);

ZVAL_ARR(return_value, props);
GC_ADDREF(props);
}

static void spl_heap_serialize_internal_state(zval *return_value, spl_heap_object *intern, bool is_pqueue)
{
zval heap_elements;
int heap_count = intern->heap->count;

array_init(return_value);
add_assoc_long(return_value, "flags", intern->flags);

if (heap_count == 0) {
return;
}

array_init_size(&heap_elements, heap_count);

for (int heap_idx = 0; heap_idx < heap_count; ++heap_idx) {
if (is_pqueue) {
spl_pqueue_elem *elem = spl_heap_elem(intern->heap, heap_idx);
zval entry;
array_init(&entry);
add_assoc_zval_ex(&entry, "data", strlen("data"), &elem->data);
Z_TRY_ADDREF(elem->data);
add_assoc_zval_ex(&entry, "priority", strlen("priority"), &elem->priority);
Z_TRY_ADDREF(elem->priority);
zend_hash_next_index_insert(Z_ARRVAL(heap_elements), &entry);
} else {
zval *elem = spl_heap_elem(intern->heap, heap_idx);
zend_hash_next_index_insert(Z_ARRVAL(heap_elements), elem);
Z_TRY_ADDREF_P(elem);
}
}

add_assoc_zval(return_value, "heap_elements", &heap_elements);
}

static void spl_heap_unserialize_properties(HashTable *props_ht, spl_heap_object *intern)
{
object_properties_load(&intern->std, props_ht);
if (!EG(exception)) {
return;
}

zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
}

static void spl_heap_unserialize_internal_state(HashTable *state_ht, spl_heap_object *intern, zval *this_ptr, bool is_pqueue)
{
zval *flags_val = zend_hash_str_find(state_ht, "flags", strlen("flags"));
if (!flags_val || Z_TYPE_P(flags_val) != IS_LONG) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
return;
}

zend_long flags_value = Z_LVAL_P(flags_val);

if (is_pqueue) {
flags_value &= SPL_PQUEUE_EXTR_MASK;
if (!flags_value) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
return;
}
}

intern->flags = (int) flags_value;

zval *heap_elements = zend_hash_str_find(state_ht, "heap_elements", strlen("heap_elements"));
if (!heap_elements) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
return;
}

if (Z_TYPE_P(heap_elements) != IS_ARRAY) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
return;
}

ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(heap_elements), zval *val) {
if (is_pqueue) {
if (Z_TYPE_P(val) != IS_ARRAY) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
return;
}

zval *data_val = zend_hash_str_find(Z_ARRVAL_P(val), "data", strlen("data") );
zval *priority_val = zend_hash_str_find(Z_ARRVAL_P(val), "priority", strlen("priority"));

if (!data_val || !priority_val) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
return;
}

spl_pqueue_elem elem;
ZVAL_COPY(&elem.data, data_val);
ZVAL_COPY(&elem.priority, priority_val);
spl_ptr_heap_insert(intern->heap, &elem, this_ptr);
} else {
Z_TRY_ADDREF_P(val);
spl_ptr_heap_insert(intern->heap, val, this_ptr);
}
} ZEND_HASH_FOREACH_END();
}

PHP_METHOD(SplPriorityQueue, __serialize)
{
spl_heap_object *intern = Z_SPLHEAP_P(ZEND_THIS);
zval props, state;

ZEND_PARSE_PARAMETERS_NONE();

array_init(return_value);

spl_heap_serialize_properties(&props, intern);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &props);

spl_heap_serialize_internal_state(&state, intern, true);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &state);
}

PHP_METHOD(SplPriorityQueue, __unserialize)
{
HashTable *data;
spl_heap_object *intern = Z_SPLHEAP_P(ZEND_THIS);

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(data)
ZEND_PARSE_PARAMETERS_END();

if (zend_hash_num_elements(data) != 2) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();
}

zval *props = zend_hash_index_find(data, 0);
if (!props || Z_TYPE_P(props) != IS_ARRAY) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();
}

spl_heap_unserialize_properties(Z_ARRVAL_P(props), intern);
if (EG(exception)) {
RETURN_THROWS();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this one you should throw to wrap the exception into the “Invalid serialization data for %s object” one. When throwing while an exception is already active, the engine will automatically set $previous:

Suggested change
RETURN_THROWS();
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case object_properties_load() sets one in the executor globals, alright I get it. PR updated

}

zval *state = zend_hash_index_find(data, 1);
if (!state || Z_TYPE_P(state) != IS_ARRAY) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();
}

spl_heap_unserialize_internal_state(Z_ARRVAL_P(state), intern, ZEND_THIS, true);
if (EG(exception)) {
RETURN_THROWS();
}
}

PHP_METHOD(SplHeap, __serialize)
{
spl_heap_object *intern = Z_SPLHEAP_P(ZEND_THIS);
zval props, state;

ZEND_PARSE_PARAMETERS_NONE();

array_init(return_value);

spl_heap_serialize_properties(&props, intern);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &props);

spl_heap_serialize_internal_state(&state, intern, false);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &state);
}

PHP_METHOD(SplHeap, __unserialize)
{
HashTable *data;
spl_heap_object *intern = Z_SPLHEAP_P(ZEND_THIS);

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(data)
ZEND_PARSE_PARAMETERS_END();

if (zend_hash_num_elements(data) != 2) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();
}

zval *props = zend_hash_index_find(data, 0);
if (!props || Z_TYPE_P(props) != IS_ARRAY) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();
}

spl_heap_unserialize_properties(Z_ARRVAL_P(props), intern);
if (EG(exception)) {
RETURN_THROWS();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}

zval *state = zend_hash_index_find(data, 1);
if (!state || Z_TYPE_P(state) != IS_ARRAY) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(intern->std.ce->name));
RETURN_THROWS();
}

spl_heap_unserialize_internal_state(Z_ARRVAL_P(state), intern, ZEND_THIS, false);
if (EG(exception)) {
RETURN_THROWS();
}
}

/* iterator handler table */
static const zend_object_iterator_funcs spl_heap_it_funcs = {
spl_heap_it_dtor,
Expand Down
12 changes: 12 additions & 0 deletions ext/spl/spl_heap.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public function getExtractFlags(): int {}

/** @tentative-return-type */
public function __debugInfo(): array {}

/** @tentative-return-type */
public function __serialize(): array {}

/** @tentative-return-type */
public function __unserialize(array $data): void {}
}

abstract class SplHeap implements Iterator, Countable
Expand Down Expand Up @@ -127,6 +133,12 @@ public function isCorrupted(): bool {}

/** @tentative-return-type */
public function __debugInfo(): array {}

/** @tentative-return-type */
public function __serialize(): array {}

/** @tentative-return-type */
public function __unserialize(array $data): void {}
}

class SplMinHeap extends SplHeap
Expand Down
20 changes: 19 additions & 1 deletion ext/spl/spl_heap_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions ext/spl/tests/SplHeap_serialization_format_security.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--TEST--
SplHeap serialization format - indexed array format prevents conflicts
--FILE--
<?php

$heap = new SplMaxHeap();
$heap->insert(100);
$heap->insert(50);
$heap->insert(200);

$serialized_data = $heap->__serialize();

echo "Serialization data structure:\n";
echo "Is indexed array: " . (isset($serialized_data[0], $serialized_data[1]) && count($serialized_data) === 2 ? 'YES' : 'NO') . "\n";
echo "Index 0 (properties) is array: " . (is_array($serialized_data[0]) ? 'YES' : 'NO') . "\n";
echo "Index 1 (internal state) is array: " . (is_array($serialized_data[1]) ? 'YES' : 'NO') . "\n";

$internal_state = $serialized_data[1];
echo "Internal state contains 'flags': " . (array_key_exists('flags', $internal_state) ? 'YES' : 'NO') . "\n";
echo "Internal state contains 'heap_elements': " . (array_key_exists('heap_elements', $internal_state) ? 'YES' : 'NO') . "\n";
echo "Flags value is numeric: " . (is_numeric($internal_state['flags']) ? 'YES' : 'NO') . "\n";
echo "Heap elements is array: " . (is_array($internal_state['heap_elements']) ? 'YES' : 'NO') . "\n";
echo "Heap elements count: " . count($internal_state['heap_elements']) . "\n";

$min_heap = new SplMinHeap();
$min_heap->insert(100);
$min_heap->insert(50);

$min_data = $min_heap->__serialize();

$pq = new SplPriorityQueue();
$pq->insert('data', 10);
$pq->setExtractFlags(SplPriorityQueue::EXTR_DATA);

$pq_data = $pq->__serialize();

$pq_internal = $pq_data[1];
if (isset($pq_internal['heap_elements']) && is_array($pq_internal['heap_elements'])) {
$first_element = $pq_internal['heap_elements'][0];
echo "PQ elements have 'data' key: " . (array_key_exists('data', $first_element) ? 'YES' : 'NO') . "\n";
echo "PQ elements have 'priority' key: " . (array_key_exists('priority', $first_element) ? 'YES' : 'NO') . "\n";
}

?>
--EXPECT--
Serialization data structure:
Is indexed array: YES
Index 0 (properties) is array: YES
Index 1 (internal state) is array: YES
Internal state contains 'flags': YES
Internal state contains 'heap_elements': YES
Flags value is numeric: YES
Heap elements is array: YES
Heap elements count: 3
PQ elements have 'data' key: YES
PQ elements have 'priority' key: YES
Loading
Loading