Skip to content

var_export emits debug assertion errors for HT_ASSERT_RC1(ht); in builds for SplFixedArray circular references #8044

Closed
@TysonAndre

Description

@TysonAndre

Description

The following code in a php build configured with --enable-debug (reproduced in 7.4, 8.0, 8.1, 8.2.0-dev):

<?php
$x = new SplFixedArray(1);
$x[0] = $x;
var_export($x);

Resulted in this output in _zend_hash_index_add_or_update_i for HT_ASSERT_RC1. This is due to ext/spl/spl_array.c needing to update the implementation of its array in place for static HashTable* spl_fixedarray_object_get_properties(zend_object *obj) so that the output of var_export, var_dump, json_encode, etc. all reflect the latest array contents of the backing fixed array:

php: /path/to/php-src/Zend/zend_hash.c:1012: _zend_hash_index_add_or_update_i: Assertion `(zend_gc_refcount(&(ht)->gc) == 1) || ((ht)->u.flags & (1<<6))' failed.
[1]    312613 abort (core dumped)  php -a

(Most of the time it will be setting the offset of the array to itself, and not actually matter, but set_error_handler can have user code modify the original properties. This is a constructed example and not really likely to matter)

But I expected this output instead:

Not crashing, successfully setting $x[0]

(I haven't tested php 7.3, though that branch is closed regardless. 7.2 did not have this debug failure)


Also, I'm not familiar with the history of this, but it seems like there are (at least) 3 different mechanisms in use for infinite recursion detection for json_encode/var_export/var_dump. Would it make sense to firstcheck Z_PROTECT_RECURSION in var_export in addition to/instead of protecting the array, before calling get_properties_for?

  • json_encode needs distinct results of *get_properties (Z_OBJPROP) to detect infinite recursion.

  • var_export needs *get_properties_for to return the same array every time to detect infinite recursion with GC_TRY_PROTECT_RECURSION. (E.g. this 1. makes it impractical to save memory (and save time in garbage collection) by returning a distinct array rather than populating get_properties., 2. Seems like it would delay garbage collection of objects unintuitively if you call var_export then replace the entry of SplFixedArray (they wouldn't be garbage collected until the next var_export/json_encode/var_dump call, or (array) cast?))

    Adding similar calls to Z_PROTECT_RECURSION(zval *struct) (and UNPROTECT), either instead of or in addition to existing may help avoid this failure mode.

    By design, var_export does not use https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo - I wonder if that's somehow related to the different design used by var_dump, but the code isn't documented

  • var_dump is doing yet another different thing, and calling Z_PROTECT_RECURSION(zval *struct) to set the GC_FLAGS
    on the object?

  • serialize thankfully calls php_var_serialize_intern and checks with var_already = php_add_var_hash with a var_hash specific to that serializer, which is correct


I'd be worried about replacing the existing checks in <=8.1/8.0, in case extensions or end users of those extensions were somehow relying on specifics of whatever calls to get_properties/get_properties_for were made.

#8041 is a different issue I also found when looking into best practices for implementing *get_properties and *get_properties_for

PHP Version

PHP 7.4-8.2

Operating System

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions