Skip to content

Commit 72ff655

Browse files
committed
Improve JSON serializable object recursion protection
1 parent a92ecdb commit 72ff655

16 files changed

+297
-40
lines changed

Diff for: Zend/zend.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ static void zend_print_zval_r_to_buf(smart_str *buf, zval *expr, int indent) /*
554554
smart_str_appendc(buf, '\n');
555555
}
556556

557-
if (GC_IS_RECURSIVE(Z_OBJ_P(expr))) {
557+
if (GC_IS_DEBUG_RECURSIVE(Z_OBJ_P(expr))) {
558558
smart_str_appends(buf, " *RECURSION*");
559559
return;
560560
}
@@ -564,9 +564,9 @@ static void zend_print_zval_r_to_buf(smart_str *buf, zval *expr, int indent) /*
564564
break;
565565
}
566566

567-
GC_PROTECT_RECURSION(Z_OBJ_P(expr));
567+
GC_PROTECT_DEBUG_RECURSION(Z_OBJ_P(expr));
568568
print_hash(buf, properties, indent, 1);
569-
GC_UNPROTECT_RECURSION(Z_OBJ_P(expr));
569+
GC_UNPROTECT_DEBUG_RECURSION(Z_OBJ_P(expr));
570570

571571
zend_release_properties(properties);
572572
break;

Diff for: Zend/zend_gc.c

+5-5
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@
7979
#endif
8080

8181
/* GC_INFO layout */
82-
#define GC_ADDRESS 0x0fffffu
83-
#define GC_COLOR 0x300000u
82+
#define GC_ADDRESS 0x07ffffu
83+
#define GC_COLOR 0x180000u
8484

8585
#define GC_BLACK 0x000000u /* must be zero */
86-
#define GC_WHITE 0x100000u
87-
#define GC_GREY 0x200000u
88-
#define GC_PURPLE 0x300000u
86+
#define GC_WHITE 0x080000u
87+
#define GC_GREY 0x100000u
88+
#define GC_PURPLE 0x180000u
8989

9090
/* Debug tracing */
9191
#if ZEND_GC_DEBUG > 1

Diff for: Zend/zend_types.h

+26-7
Original file line numberDiff line numberDiff line change
@@ -642,10 +642,10 @@ static zend_always_inline zend_uchar zval_get_type(const zval* pz) {
642642
#define GC_TRY_DELREF(p) zend_gc_try_delref(&(p)->gc)
643643

644644
#define GC_TYPE_MASK 0x0000000f
645-
#define GC_FLAGS_MASK 0x000003f0
646-
#define GC_INFO_MASK 0xfffffc00
645+
#define GC_FLAGS_MASK 0x000007f0
646+
#define GC_INFO_MASK 0xfffff800
647647
#define GC_FLAGS_SHIFT 0
648-
#define GC_INFO_SHIFT 10
648+
#define GC_INFO_SHIFT 11
649649

650650
static zend_always_inline zend_uchar zval_gc_type(uint32_t gc_type_info) {
651651
return (gc_type_info & GC_TYPE_MASK);
@@ -684,10 +684,11 @@ static zend_always_inline uint32_t zval_gc_info(uint32_t gc_type_info) {
684684

685685
/* zval_gc_flags(zval.value->gc.u.type_info) (common flags) */
686686
#define GC_NOT_COLLECTABLE (1<<4)
687-
#define GC_PROTECTED (1<<5) /* used for recursion detection */
688-
#define GC_IMMUTABLE (1<<6) /* can't be changed in place */
689-
#define GC_PERSISTENT (1<<7) /* allocated using malloc */
690-
#define GC_PERSISTENT_LOCAL (1<<8) /* persistent, but thread-local */
687+
#define GC_PROTECTED (1<<5) /* used for recursion detection */
688+
#define GC_IMMUTABLE (1<<6) /* can't be changed in place */
689+
#define GC_PERSISTENT (1<<7) /* allocated using malloc */
690+
#define GC_PERSISTENT_LOCAL (1<<8) /* persistent, but thread-local */
691+
#define GC_DEBUG_PROTECTED (1<<10) /* whether the protection is for debug functions */
691692

692693
#define GC_NULL (IS_NULL | (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT))
693694
#define GC_STRING (IS_STRING | (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT))
@@ -789,6 +790,24 @@ static zend_always_inline uint32_t zval_gc_info(uint32_t gc_type_info) {
789790
#define Z_PROTECT_RECURSION_P(zv) Z_PROTECT_RECURSION(*(zv))
790791
#define Z_UNPROTECT_RECURSION_P(zv) Z_UNPROTECT_RECURSION(*(zv))
791792

793+
#define GC_IS_DEBUG_RECURSIVE(p) \
794+
((GC_FLAGS(p) & GC_PROTECTED) && (GC_FLAGS(p) & GC_DEBUG_PROTECTED))
795+
796+
#define GC_PROTECT_DEBUG_RECURSION(p) do { \
797+
GC_ADD_FLAGS(p, (GC_PROTECTED | GC_DEBUG_PROTECTED)); \
798+
} while (0)
799+
800+
#define GC_UNPROTECT_DEBUG_RECURSION(p) do { \
801+
GC_DEL_FLAGS(p, (GC_PROTECTED | GC_DEBUG_PROTECTED)); \
802+
} while (0)
803+
804+
#define Z_IS_DEBUG_RECURSIVE(zval) GC_IS_DEBUG_RECURSIVE(Z_COUNTED(zval))
805+
#define Z_PROTECT_DEBUG_RECURSION(zval) GC_PROTECT_DEBUG_RECURSION(Z_COUNTED(zval))
806+
#define Z_UNPROTECT_DEBUG_RECURSION(zval) GC_UNPROTECT_DEBUG_RECURSION(Z_COUNTED(zval))
807+
#define Z_IS_DEBUG_RECURSIVE_P(zv) Z_IS_DEBUG_RECURSIVE(*(zv))
808+
#define Z_PROTECT_DEBUG_RECURSION_P(zv) Z_PROTECT_DEBUG_RECURSION(*(zv))
809+
#define Z_UNPROTECT_DEBUG_RECURSION_P(zv) Z_UNPROTECT_DEBUG_RECURSION(*(zv))
810+
792811
/* All data types < IS_STRING have their constructor/destructors skipped */
793812
#define Z_CONSTANT(zval) (Z_TYPE(zval) == IS_CONSTANT_AST)
794813
#define Z_CONSTANT_P(zval_p) Z_CONSTANT(*(zval_p))

Diff for: ext/json/json_encoder.c

+25-6
Original file line numberDiff line numberDiff line change
@@ -532,10 +532,29 @@ static zend_result php_json_escape_string(
532532
}
533533
/* }}} */
534534

535+
#define PHP_JSON_SERIALIZABLE_OBJECT_PROTECT_RECURSION(_obj, _ht) \
536+
do { \
537+
if (UNEXPECTED(_ht)) { \
538+
GC_PROTECT_RECURSION(_ht); \
539+
} else { \
540+
GC_PROTECT_RECURSION(_obj); \
541+
} \
542+
} while (0)
543+
544+
#define PHP_JSON_SERIALIZABLE_OBJECT_UNPROTECT_RECURSION(_obj, _ht) \
545+
do { \
546+
if (UNEXPECTED(_ht)) { \
547+
GC_UNPROTECT_RECURSION(_ht); \
548+
} else { \
549+
GC_UNPROTECT_RECURSION(_obj); \
550+
} \
551+
} while (0)
552+
535553
static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val, int options, php_json_encoder *encoder) /* {{{ */
536554
{
537555
zend_class_entry *ce = Z_OBJCE_P(val);
538-
HashTable* myht = Z_OBJPROP_P(val);
556+
zend_object *obj = Z_OBJ_P(val);
557+
HashTable* myht = GC_IS_RECURSIVE(obj) ? Z_OBJPROP_P(val) : NULL;
539558
zval retval, fname;
540559
zend_result return_code;
541560

@@ -547,7 +566,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val
547566
return FAILURE;
548567
}
549568

550-
PHP_JSON_HASH_PROTECT_RECURSION(myht);
569+
PHP_JSON_SERIALIZABLE_OBJECT_PROTECT_RECURSION(obj, myht);
551570

552571
ZVAL_STRING(&fname, "jsonSerialize");
553572

@@ -560,7 +579,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val
560579
if (options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) {
561580
smart_str_appendl(buf, "null", 4);
562581
}
563-
PHP_JSON_HASH_UNPROTECT_RECURSION(myht);
582+
PHP_JSON_SERIALIZABLE_OBJECT_UNPROTECT_RECURSION(obj, myht);
564583
return FAILURE;
565584
}
566585

@@ -572,19 +591,19 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val
572591
if (options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) {
573592
smart_str_appendl(buf, "null", 4);
574593
}
575-
PHP_JSON_HASH_UNPROTECT_RECURSION(myht);
594+
PHP_JSON_SERIALIZABLE_OBJECT_UNPROTECT_RECURSION(obj, myht);
576595
return FAILURE;
577596
}
578597

579598
if ((Z_TYPE(retval) == IS_OBJECT) &&
580599
(Z_OBJ(retval) == Z_OBJ_P(val))) {
581600
/* Handle the case where jsonSerialize does: return $this; by going straight to encode array */
582-
PHP_JSON_HASH_UNPROTECT_RECURSION(myht);
601+
PHP_JSON_SERIALIZABLE_OBJECT_UNPROTECT_RECURSION(obj, myht);
583602
return_code = php_json_encode_array(buf, &retval, options, encoder);
584603
} else {
585604
/* All other types, encode as normal */
586605
return_code = php_json_encode_zval(buf, &retval, options, encoder);
587-
PHP_JSON_HASH_UNPROTECT_RECURSION(myht);
606+
PHP_JSON_SERIALIZABLE_OBJECT_UNPROTECT_RECURSION(obj, myht);
588607
}
589608

590609
zval_ptr_dtor(&retval);

Diff for: ext/json/tests/bug61978.phpt

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ var_dump(json_encode($obj2, JSON_PARTIAL_OUTPUT_ON_ERROR));
3838
--EXPECT--
3939
string(24) "{"test":"123","me":null}"
4040
==
41-
string(24) "{"test":"123","me":null}"
41+
string(44) "{"test":"123","me":{"test":"123","me":null}}"

Diff for: ext/json/tests/json_encode_recursion_01.phpt

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
json_encode() Recursion test with just JsonSerializable
3+
--FILE--
4+
<?php
5+
6+
class SerializingTest implements JsonSerializable
7+
{
8+
public $a = 1;
9+
10+
private $b = 'hide';
11+
12+
protected $c = 'protect';
13+
14+
public function jsonSerialize(): mixed
15+
{
16+
$result = json_encode($this);
17+
var_dump($result);
18+
return $this;
19+
}
20+
}
21+
22+
var_dump(json_encode(new SerializingTest()));
23+
?>
24+
--EXPECT--
25+
bool(false)
26+
string(7) "{"a":1}"
27+
string(7) "{"a":1}"

Diff for: ext/json/tests/json_encode_recursion_02.phpt

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
json_encode() Recursion test with JsonSerializable and var_dump simple
3+
--FILE--
4+
<?php
5+
6+
class SerializingTest implements JsonSerializable
7+
{
8+
public $a = 1;
9+
10+
public function jsonSerialize(): mixed
11+
{
12+
var_dump($this);
13+
return $this;
14+
}
15+
}
16+
17+
var_dump(json_encode(new SerializingTest()));
18+
19+
?>
20+
--EXPECT--
21+
object(SerializingTest)#1 (1) {
22+
["a"]=>
23+
int(1)
24+
}
25+
string(7) "{"a":1}"

Diff for: ext/json/tests/json_encode_recursion_03.phpt

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
json_encode() Recursion test with JsonSerializable and __debugInfo
3+
--FILE--
4+
<?php
5+
6+
class SerializingTest implements JsonSerializable
7+
{
8+
public $a = 1;
9+
10+
public function __debugInfo()
11+
{
12+
return [ 'result' => json_encode($this) ];
13+
}
14+
15+
public function jsonSerialize(): mixed
16+
{
17+
var_dump($this);
18+
return $this;
19+
}
20+
}
21+
22+
var_dump(json_encode(new SerializingTest()));
23+
echo "---------\n";
24+
var_dump(new SerializingTest());
25+
26+
?>
27+
--EXPECT--
28+
*RECURSION*
29+
object(SerializingTest)#1 (1) {
30+
["result"]=>
31+
string(7) "{"a":1}"
32+
}
33+
string(7) "{"a":1}"
34+
---------
35+
*RECURSION*
36+
object(SerializingTest)#1 (1) {
37+
["result"]=>
38+
string(7) "{"a":1}"
39+
}

Diff for: ext/json/tests/json_encode_recursion_04.phpt

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
json_encode() Recursion test with JsonSerializable, __debugInfo and var_export
3+
--FILE--
4+
<?php
5+
6+
class SerializingTest implements JsonSerializable
7+
{
8+
public $a = 1;
9+
10+
public function __debugInfo()
11+
{
12+
return [ 'result' => var_export($this, true) ];
13+
}
14+
15+
public function jsonSerialize(): mixed
16+
{
17+
var_dump($this);
18+
return $this;
19+
}
20+
}
21+
22+
var_dump(json_encode(new SerializingTest()));
23+
echo "---------\n";
24+
var_dump(new SerializingTest());
25+
26+
?>
27+
--EXPECT--
28+
object(SerializingTest)#1 (1) {
29+
["result"]=>
30+
string(52) "\SerializingTest::__set_state(array(
31+
'a' => 1,
32+
))"
33+
}
34+
string(7) "{"a":1}"
35+
---------
36+
object(SerializingTest)#1 (1) {
37+
["result"]=>
38+
string(52) "\SerializingTest::__set_state(array(
39+
'a' => 1,
40+
))"
41+
}

Diff for: ext/json/tests/json_encode_recursion_05.phpt

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
json_encode() Recursion test with JsonSerializable, __debugInfo and print_r
3+
--FILE--
4+
<?php
5+
6+
class SerializingTest implements JsonSerializable
7+
{
8+
public $a = 1;
9+
10+
public function __debugInfo()
11+
{
12+
return [ 'result' => $this->a ];
13+
}
14+
15+
public function jsonSerialize(): mixed
16+
{
17+
print_r($this);
18+
return $this;
19+
}
20+
}
21+
22+
var_dump(json_encode(new SerializingTest()));
23+
echo "---------\n";
24+
var_dump(new SerializingTest());
25+
26+
?>
27+
--EXPECT--
28+
SerializingTest Object
29+
(
30+
[result] => 1
31+
)
32+
string(7) "{"a":1}"
33+
---------
34+
object(SerializingTest)#1 (1) {
35+
["result"]=>
36+
int(1)
37+
}

Diff for: ext/json/tests/json_encode_recursion_06.phpt

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
json_encode() Recursion test with JsonSerializable and serialize
3+
--FILE--
4+
<?php
5+
6+
class JsonEncodeFirstTest implements JsonSerializable
7+
{
8+
public $a = 1;
9+
10+
public function __serialize()
11+
{
12+
return [ 'result' => $this->a ];
13+
}
14+
15+
public function jsonSerialize(): mixed
16+
{
17+
return [ 'serialize' => serialize($this) ];
18+
}
19+
}
20+
21+
class SerializeFirstTest implements JsonSerializable
22+
{
23+
public $a = 1;
24+
25+
public function __serialize()
26+
{
27+
return [ 'result' => json_encode($this) ];
28+
}
29+
30+
public function jsonSerialize(): mixed
31+
{
32+
return [ 'json' => serialize($this) ];
33+
}
34+
}
35+
36+
var_dump(json_encode(new JsonEncodeFirstTest()));
37+
var_dump(serialize(new SerializeFirstTest()));
38+
?>
39+
--EXPECT--
40+
string(68) "{"serialize":"O:19:\"JsonEncodeFirstTest\":1:{s:6:\"result\";i:1;}"}"
41+
string(194) "O:18:"SerializeFirstTest":1:{s:6:"result";s:142:"{"json":"O:18:\"SerializeFirstTest\":1:{s:6:\"result\";s:62:\"{\"json\":\"O:18:\\\"SerializeFirstTest\\\":1:{s:6:\\\"result\\\";b:0;}\"}\";}"}";}"

0 commit comments

Comments
 (0)