Skip to content

Commit c4ecd82

Browse files
authored
Make inspecting SplFixedArray instances more memory efficient/consistent, change print_r null props handling (#9757)
* Make handling of SplFixedArray properties more consistent Create a brand new reference counted array every time in SplFixedArray to be freed by the callers (or return null). Switch from overriding `get_properties` to overriding `get_properties_for` handler * Print objects with null hash table like others in print_r Noticed when working on subsequent commits for SplFixedArray. Make whether zend_get_properties_for returns null or an empty array invisible to the end user - it would be always be a non-null array for user-defined classes. Always print newlines with `\n\s*(\n\s*)` after objects Noticed when working on SplFixedArray changes, e.g. in ext/spl/tests/SplFixedArray__construct_param_null.phpt
1 parent 6e8f2ba commit c4ecd82

8 files changed

+190
-52
lines changed

Diff for: Zend/tests/function_arguments/sensitive_parameter_value.phpt

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ object(SensitiveParameterValue)#%d (0) {
3131
object(SensitiveParameterValue)#%d (%d) refcount(%d){
3232
}
3333
SensitiveParameterValue Object
34+
(
35+
)
3436

3537
# var_export()
3638
\SensitiveParameterValue::__set_state(array(

Diff for: Zend/zend.c

+1
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ static void zend_print_zval_r_to_buf(smart_str *buf, zval *expr, int indent) /*
507507
}
508508

509509
if ((properties = zend_get_properties_for(expr, ZEND_PROP_PURPOSE_DEBUG)) == NULL) {
510+
print_hash(buf, (HashTable*) &zend_empty_array, indent, 1);
510511
break;
511512
}
512513

Diff for: ext/spl/spl_fixedarray.c

+32-47
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ typedef struct _spl_fixedarray {
4848
zend_long size;
4949
/* It is possible to resize this, so this can't be combined with the object */
5050
zval *elements;
51-
/* True if this was modified after the last call to get_properties or the hash table wasn't rebuilt. */
52-
bool should_rebuild_properties;
5351
} spl_fixedarray;
5452

5553
typedef struct _spl_fixedarray_object {
@@ -107,7 +105,6 @@ static void spl_fixedarray_init_non_empty_struct(spl_fixedarray *array, zend_lon
107105
array->size = 0; /* reset size in case ecalloc() fails */
108106
array->elements = size ? safe_emalloc(size, sizeof(zval), 0) : NULL;
109107
array->size = size;
110-
array->should_rebuild_properties = true;
111108
}
112109

113110
static void spl_fixedarray_init(spl_fixedarray *array, zend_long size)
@@ -177,7 +174,6 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
177174
/* nothing to do */
178175
return;
179176
}
180-
array->should_rebuild_properties = true;
181177

182178
/* first initialization */
183179
if (array->size == 0) {
@@ -211,47 +207,41 @@ static HashTable* spl_fixedarray_object_get_gc(zend_object *obj, zval **table, i
211207
return ht;
212208
}
213209

214-
static HashTable* spl_fixedarray_object_get_properties(zend_object *obj)
210+
static HashTable* spl_fixedarray_object_get_properties_for(zend_object *obj, zend_prop_purpose purpose)
215211
{
216-
spl_fixedarray_object *intern = spl_fixed_array_from_obj(obj);
217-
HashTable *ht = zend_std_get_properties(obj);
218-
219-
if (!spl_fixedarray_empty(&intern->array)) {
220-
/*
221-
* Usually, the reference count of the hash table is 1,
222-
* except during cyclic reference cycles.
223-
*
224-
* Maintain the DEBUG invariant that a hash table isn't modified during iteration,
225-
* and avoid unnecessary work rebuilding a hash table for unmodified properties.
226-
*
227-
* See https://github.com/php/php-src/issues/8079 and ext/spl/tests/fixedarray_022.phpt
228-
* Also see https://github.com/php/php-src/issues/8044 for alternate considered approaches.
229-
*/
230-
if (!intern->array.should_rebuild_properties) {
231-
/* Return the same hash table so that recursion cycle detection works in internal functions. */
232-
return ht;
233-
}
234-
intern->array.should_rebuild_properties = false;
212+
/* This has __serialize, so the purpose is not ZEND_PROP_PURPOSE_SERIALIZE, which would expect a non-null return value */
213+
ZEND_ASSERT(purpose != ZEND_PROP_PURPOSE_SERIALIZE);
235214

236-
zend_long j = zend_hash_num_elements(ht);
215+
const spl_fixedarray_object *intern = spl_fixed_array_from_obj(obj);
216+
/*
217+
* SplFixedArray can be subclassed or have dynamic properties (With or without AllowDynamicProperties in subclasses).
218+
* Instances of subclasses with declared properties may have properties but not yet have a property table.
219+
*/
220+
HashTable *source_properties = obj->properties ? obj->properties : (obj->ce->default_properties_count ? zend_std_get_properties(obj) : NULL);
237221

238-
if (GC_REFCOUNT(ht) > 1) {
239-
intern->std.properties = zend_array_dup(ht);
240-
GC_TRY_DELREF(ht);
241-
}
242-
for (zend_long i = 0; i < intern->array.size; i++) {
243-
zend_hash_index_update(ht, i, &intern->array.elements[i]);
244-
Z_TRY_ADDREF(intern->array.elements[i]);
245-
}
246-
if (j > intern->array.size) {
247-
for (zend_long i = intern->array.size; i < j; ++i) {
248-
zend_hash_index_del(ht, i);
222+
const zend_long size = intern->array.size;
223+
if (size == 0 && (!source_properties || !zend_hash_num_elements(source_properties))) {
224+
return NULL;
225+
}
226+
zval *const elements = intern->array.elements;
227+
HashTable *ht = zend_new_array(size);
228+
229+
for (zend_long i = 0; i < size; i++) {
230+
Z_TRY_ADDREF_P(&elements[i]);
231+
zend_hash_next_index_insert(ht, &elements[i]);
232+
}
233+
if (source_properties && zend_hash_num_elements(source_properties) > 0) {
234+
zend_long nkey;
235+
zend_string *skey;
236+
zval *value;
237+
ZEND_HASH_MAP_FOREACH_KEY_VAL_IND(source_properties, nkey, skey, value) {
238+
Z_TRY_ADDREF_P(value);
239+
if (skey) {
240+
zend_hash_add_new(ht, skey, value);
241+
} else {
242+
zend_hash_index_update(ht, nkey, value);
249243
}
250-
}
251-
if (HT_IS_PACKED(ht)) {
252-
/* Engine doesn't expet packed array */
253-
zend_hash_packed_to_hash(ht);
254-
}
244+
} ZEND_HASH_FOREACH_END();
255245
}
256246

257247
return ht;
@@ -394,9 +384,6 @@ static zval *spl_fixedarray_object_read_dimension(zend_object *object, zval *off
394384
}
395385

396386
spl_fixedarray_object *intern = spl_fixed_array_from_obj(object);
397-
if (type != BP_VAR_IS && type != BP_VAR_R) {
398-
intern->array.should_rebuild_properties = true;
399-
}
400387
return spl_fixedarray_object_read_dimension_helper(intern, offset);
401388
}
402389

@@ -420,7 +407,6 @@ static void spl_fixedarray_object_write_dimension_helper(spl_fixedarray_object *
420407
zend_throw_exception(spl_ce_RuntimeException, "Index invalid or out of range", 0);
421408
return;
422409
} else {
423-
intern->array.should_rebuild_properties = true;
424410
/* Fix #81429 */
425411
zval *ptr = &(intern->array.elements[index]);
426412
zval tmp;
@@ -461,7 +447,6 @@ static void spl_fixedarray_object_unset_dimension_helper(spl_fixedarray_object *
461447
zend_throw_exception(spl_ce_RuntimeException, "Index invalid or out of range", 0);
462448
return;
463449
} else {
464-
intern->array.should_rebuild_properties = true;
465450
zval_ptr_dtor(&(intern->array.elements[index]));
466451
ZVAL_NULL(&intern->array.elements[index]);
467452
}
@@ -973,7 +958,7 @@ PHP_MINIT_FUNCTION(spl_fixedarray)
973958
spl_handler_SplFixedArray.unset_dimension = spl_fixedarray_object_unset_dimension;
974959
spl_handler_SplFixedArray.has_dimension = spl_fixedarray_object_has_dimension;
975960
spl_handler_SplFixedArray.count_elements = spl_fixedarray_object_count_elements;
976-
spl_handler_SplFixedArray.get_properties = spl_fixedarray_object_get_properties;
961+
spl_handler_SplFixedArray.get_properties_for = spl_fixedarray_object_get_properties_for;
977962
spl_handler_SplFixedArray.get_gc = spl_fixedarray_object_get_gc;
978963
spl_handler_SplFixedArray.free_obj = spl_fixedarray_object_free_storage;
979964

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
SplFixedArray properties is compatible with ArrayObject
3+
--FILE--
4+
<?php
5+
$ao = new ArrayObject([1, 2, 3]);
6+
$fixedArray = new SplFixedArray(1);
7+
$fixedArray[0] = new SplDoublyLinkedList();
8+
$ao->exchangeArray($fixedArray);
9+
$ao[0] = new stdClass();
10+
var_dump($ao);
11+
?>
12+
--EXPECT--
13+
object(ArrayObject)#1 (1) {
14+
["storage":"ArrayObject":private]=>
15+
object(SplFixedArray)#2 (2) {
16+
[0]=>
17+
object(SplDoublyLinkedList)#3 (2) {
18+
["flags":"SplDoublyLinkedList":private]=>
19+
int(0)
20+
["dllist":"SplDoublyLinkedList":private]=>
21+
array(0) {
22+
}
23+
}
24+
["0"]=>
25+
object(stdClass)#4 (0) {
26+
}
27+
}
28+
}

Diff for: ext/spl/tests/ArrayObject_overloaded_object_incompatible.phpt

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ Objects with overloaded get_properties are incompatible with ArrayObject
55

66
$ao = new ArrayObject([1, 2, 3]);
77
try {
8-
$ao->exchangeArray(new SplFixedArray);
8+
$ao->exchangeArray(new DateInterval('P1D'));
99
} catch (Exception $e) {
1010
echo $e->getMessage(), "\n";
1111
}
1212
var_dump($ao);
1313

1414
?>
1515
--EXPECT--
16-
Overloaded object of type SplFixedArray is not compatible with ArrayObject
16+
Overloaded object of type DateInterval is not compatible with ArrayObject
1717
object(ArrayObject)#1 (1) {
1818
["storage":"ArrayObject":private]=>
1919
array(3) {

Diff for: ext/spl/tests/SplFixedArray_get_properties_for.phpt

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
--TEST--
2+
SplFixedArray - get_properties_for handlers
3+
--FILE--
4+
<?php
5+
#[AllowDynamicProperties]
6+
class MySplFixedArray extends SplFixedArray {
7+
public $x;
8+
public int $y;
9+
}
10+
class X {}
11+
class Y {}
12+
$array = new MySplFixedArray(2);
13+
var_dump(get_mangled_object_vars($array));
14+
$array[0] = new stdClass();
15+
$array[1] = new Y();
16+
$array->x = new SplFixedArray();
17+
$array->{0} = new X();
18+
var_dump($array);
19+
// As of php 8.3, get_mangled_object_vars only contains object properties (dynamic properties and declared subclass properties)
20+
// (Array elements in the SplFixedArray are deliberately excluded)
21+
// Before php 8.3, this would have array elements get removed in some cases but not others.
22+
var_dump(get_mangled_object_vars($array));
23+
echo "cast to array\n";
24+
var_dump((array)$array); // Adds the values from the underlying array, then the declared/dynamic object properties
25+
echo json_encode($array), "\n"; // From JsonSerializable::serialize()
26+
$ser = serialize($array);
27+
echo "$ser\n";
28+
// NOTE: The unserialize behavior for the property that is the string '0' is just because unserialize()
29+
// is coercing '0' to a string before calling SplFixedArray::__unserialize.
30+
//
31+
// Typical code would not use 0 as a property name, this test is just testing edge cases have proper reference counting and so on.
32+
var_dump(unserialize($ser));
33+
?>
34+
--EXPECT--
35+
array(1) {
36+
["x"]=>
37+
NULL
38+
}
39+
object(MySplFixedArray)#1 (4) {
40+
[0]=>
41+
object(stdClass)#2 (0) {
42+
}
43+
[1]=>
44+
object(Y)#3 (0) {
45+
}
46+
["x"]=>
47+
object(SplFixedArray)#4 (0) {
48+
}
49+
["0"]=>
50+
object(X)#5 (0) {
51+
}
52+
}
53+
array(2) {
54+
["x"]=>
55+
object(SplFixedArray)#4 (0) {
56+
}
57+
[0]=>
58+
object(X)#5 (0) {
59+
}
60+
}
61+
cast to array
62+
array(3) {
63+
[0]=>
64+
object(X)#5 (0) {
65+
}
66+
[1]=>
67+
object(Y)#3 (0) {
68+
}
69+
["x"]=>
70+
object(SplFixedArray)#4 (0) {
71+
}
72+
}
73+
[{},{}]
74+
O:15:"MySplFixedArray":5:{i:0;O:8:"stdClass":0:{}i:1;O:1:"Y":0:{}s:1:"x";i:0;s:1:"y";i:0;s:1:"0";O:1:"X":0:{}}
75+
object(MySplFixedArray)#6 (4) {
76+
[0]=>
77+
object(X)#9 (0) {
78+
}
79+
[1]=>
80+
object(Y)#8 (0) {
81+
}
82+
["x"]=>
83+
int(0)
84+
["y"]=>
85+
int(0)
86+
}

Diff for: ext/spl/tests/SplFixedArray_immediate_gc.phpt

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
SplFixedArray - values should be gced after var_export then being modified
3+
--FILE--
4+
<?php
5+
class HasDestructor {
6+
public function __destruct() {
7+
echo "In destructor\n";
8+
}
9+
}
10+
$array = SplFixedArray::fromArray([new HasDestructor()]);
11+
var_dump($array);
12+
echo "Replacing\n";
13+
$array[0] = new stdClass();
14+
// As of php 8.3, this will be freed before the var_dump call
15+
echo "Dumping again\n";
16+
var_dump($array);
17+
// As of php 8.3, this only contain object properties (dynamic properties and declared subclass properties)
18+
var_dump(get_mangled_object_vars($array));
19+
?>
20+
--EXPECT--
21+
object(SplFixedArray)#2 (1) {
22+
[0]=>
23+
object(HasDestructor)#1 (0) {
24+
}
25+
}
26+
Replacing
27+
In destructor
28+
Dumping again
29+
object(SplFixedArray)#2 (1) {
30+
[0]=>
31+
object(stdClass)#3 (0) {
32+
}
33+
}
34+
array(0) {
35+
}

Diff for: ext/spl/tests/SplFixedArray_setSize_destruct.phpt

+4-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class HasDestructor {
1212
global $values;
1313
var_dump($values);
1414
$values->setSize($values->getSize() - 1);
15+
echo "After reducing size:\n";
1516
var_dump($values);
1617
}
1718
}
@@ -24,9 +25,8 @@ object(SplFixedArray)#1 (1) {
2425
[0]=>
2526
bool(false)
2627
}
27-
object(SplFixedArray)#1 (1) {
28-
[0]=>
29-
bool(false)
28+
After reducing size:
29+
object(SplFixedArray)#1 (0) {
3030
}
3131
Done
3232
Done
@@ -43,6 +43,7 @@ object(SplFixedArray)#1 (5) {
4343
object(HasDestructor)#2 (0) {
4444
}
4545
}
46+
After reducing size:
4647
object(SplFixedArray)#1 (4) {
4748
[0]=>
4849
NULL

0 commit comments

Comments
 (0)