Search code examples
phprecursionclonestack-overflow

Encoding clone $this in JsonSerializable


This simplified case is resulting in a PHP segfault (exit 127):

class Datum implements \JsonSerializable{
  public function jsonSerialize(){
    return clone $this;
  }
}
echo json_encode(new Datum);

The last line of code results in exit(127). I'm unable to retrieve any stack in my current environment.

Meanwhile, removing the clone token works.

Is there any possible explanation why this is happening?


Solution

  • This code results in an infinite recursion.

    It appears that the PHP JSON module supports JsonSerializable in this manner (pseudocode):

    function json_encode($data){
        if($data instanceof JsonSerializable) return json_encode($data->jsonSerialize());
        else real_json_encode($data); // handling primitive data or arrays or pure data objects
    }
    

    If you return yet another instance of JsonSerializable, json_encode is going to try to serialize it again, resulting in an infinite recursion.

    This is working for return $this;, however, probably due to intentional workaround from json_encode's implementation where it goes straight to real json_encode when the returned object is identical, i.e. when $this is returned. However this is not happening for cloned objects since $a !== clone $a.

    References

    This answer can be supported by reference from the php-src.

    // in php_json_encode_zval
    if (instanceof_function(Z_OBJCE_P(val), php_json_serializable_ce)) {
        return php_json_encode_serializable_object(buf, val, options, encoder);
    }
    
    // in php_json_encode_serializable_object
    if ((Z_TYPE(retval) == IS_OBJECT) &&
        (Z_OBJ(retval) == Z_OBJ_P(val))) {
        /* Handle the case where jsonSerialize does: return $this; by going straight to encode array */
        PHP_JSON_HASH_APPLY_PROTECTION_DEC(myht);
        return_code = php_json_encode_array(buf, &retval, options, encoder);
    } else {
        /* All other types, encode as normal */
        return_code = php_json_encode_zval(buf, &retval, options, encoder);
        PHP_JSON_HASH_APPLY_PROTECTION_DEC(myht);
    }
    

    These snippets prove that PHP would encode return $this; as an array (or as a non-serializable object), while returning anything else makes Z_OBJ(retval) == Z_OBJ_P(val) false, going to the else block which recursively calls php_json_encode_zval again.

    TL;DR, Simple solution: return (array) $this; instead of clone $this;.