Skip to content

Commit 687ad19

Browse files
committed
Fix #18542: unserialize() respects class_alias for private properties
When a class with private properties is serialized, the property names are mangled as \0ClassName\0propName. If the class is later aliased via class_alias(), unserialize() fails to match the mangled class name against the actual class entry, causing properties to be treated as dynamic instead of declared. This adds a fallback check in is_property_visibility_changed() that uses zend_hash_str_find_ptr_lc() to look up the mangled class name in EG(class_table) and verify it resolves to the same zend_class_entry. This is only triggered when the direct name comparison fails, so there is zero overhead on the normal (non-alias) path. Closes GH-18542
1 parent e22ba55 commit 687ad19

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
--TEST--
2+
unserialize() respects class_alias for private properties
3+
--FILE--
4+
<?php
5+
6+
// Test 1: Basic class_alias with private property
7+
class HelloAlias {
8+
public function __construct(
9+
private readonly int $answer = 0
10+
) {}
11+
12+
public function getAnswer(): int {
13+
return $this->answer;
14+
}
15+
}
16+
class_alias(HelloAlias::class, 'Hello');
17+
18+
$serialized = 'O:5:"Hello":1:{s:13:"' . "\0Hello\0" . 'answer";i:42;}';
19+
$obj = unserialize($serialized);
20+
var_dump($obj instanceof HelloAlias);
21+
var_dump($obj->getAnswer());
22+
23+
// Test 2: Protected property (should continue to work)
24+
class ProtoAlias {
25+
public function __construct(
26+
protected int $value = 0
27+
) {}
28+
public function getValue(): int { return $this->value; }
29+
}
30+
class_alias(ProtoAlias::class, 'Proto');
31+
32+
$serialized2 = 'O:5:"Proto":1:{s:8:"' . "\0*\0" . 'value";i:99;}';
33+
$obj2 = unserialize($serialized2);
34+
var_dump($obj2 instanceof ProtoAlias);
35+
var_dump($obj2->getValue());
36+
37+
// Test 3: Public property (should continue to work)
38+
class PubAlias {
39+
public int $data = 0;
40+
}
41+
class_alias(PubAlias::class, 'Pub');
42+
43+
$serialized3 = 'O:3:"Pub":1:{s:4:"data";i:77;}';
44+
$obj3 = unserialize($serialized3);
45+
var_dump($obj3 instanceof PubAlias);
46+
var_dump($obj3->data);
47+
48+
// Test 4: Inheritance — child class with parent's private property via alias
49+
class ParentClass {
50+
public function __construct(
51+
private int $secret = 0
52+
) {}
53+
public function getSecret(): int { return $this->secret; }
54+
}
55+
56+
class ChildClass extends ParentClass {
57+
public function __construct(
58+
private int $childProp = 0,
59+
int $secret = 0
60+
) {
61+
parent::__construct($secret);
62+
}
63+
public function getChildProp(): int { return $this->childProp; }
64+
}
65+
class_alias(ChildClass::class, 'Kid');
66+
67+
// Serialized with alias name for the child's private prop, canonical name for parent's
68+
$serialized4 = 'O:3:"Kid":2:{s:14:"' . "\0Kid\0" . 'childProp";i:10;s:19:"' . "\0ParentClass\0" . 'secret";i:20;}';
69+
$obj4 = unserialize($serialized4);
70+
var_dump($obj4 instanceof ChildClass);
71+
var_dump($obj4->getChildProp());
72+
var_dump($obj4->getSecret());
73+
74+
// Test 5: Multiple private properties with alias
75+
class MultiPropAlias {
76+
public function __construct(
77+
private int $x = 0,
78+
private string $y = '',
79+
private bool $z = false
80+
) {}
81+
public function getX(): int { return $this->x; }
82+
public function getY(): string { return $this->y; }
83+
public function getZ(): bool { return $this->z; }
84+
}
85+
class_alias(MultiPropAlias::class, 'Multi');
86+
87+
$serialized5 = 'O:5:"Multi":3:{s:8:"' . "\0Multi\0" . 'x";i:1;s:8:"' . "\0Multi\0" . 'y";s:3:"abc";s:8:"' . "\0Multi\0" . 'z";b:1;}';
88+
$obj5 = unserialize($serialized5);
89+
var_dump($obj5 instanceof MultiPropAlias);
90+
var_dump($obj5->getX());
91+
var_dump($obj5->getY());
92+
var_dump($obj5->getZ());
93+
94+
// Test 6: Canonical name still works (non-alias path, regression check)
95+
$serialized6 = 'O:14:"MultiPropAlias":3:{s:17:"' . "\0MultiPropAlias\0" . 'x";i:5;s:17:"' . "\0MultiPropAlias\0" . 'y";s:2:"hi";s:17:"' . "\0MultiPropAlias\0" . 'z";b:0;}';
96+
$obj6 = unserialize($serialized6);
97+
var_dump($obj6 instanceof MultiPropAlias);
98+
var_dump($obj6->getX());
99+
var_dump($obj6->getY());
100+
var_dump($obj6->getZ());
101+
102+
echo "Done\n";
103+
?>
104+
--EXPECT--
105+
bool(true)
106+
int(42)
107+
bool(true)
108+
int(99)
109+
bool(true)
110+
int(77)
111+
bool(true)
112+
int(10)
113+
int(20)
114+
bool(true)
115+
int(1)
116+
string(3) "abc"
117+
bool(true)
118+
bool(true)
119+
int(5)
120+
string(2) "hi"
121+
bool(false)
122+
Done

ext/standard/var_unserializer.re

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,8 @@ static int is_property_visibility_changed(zend_class_entry *ce, zval *key)
546546
existing_propinfo = zend_hash_find_ptr(&ce->properties_info, Z_STR_P(key));
547547
} else {
548548
if (!strcmp(unmangled_class, "*")
549-
|| !strcasecmp(unmangled_class, ZSTR_VAL(ce->name))) {
549+
|| !strcasecmp(unmangled_class, ZSTR_VAL(ce->name))
550+
|| zend_hash_str_find_ptr_lc(EG(class_table), unmangled_class, strlen(unmangled_class)) == ce) {
550551
existing_propinfo = zend_hash_str_find_ptr(
551552
&ce->properties_info, unmangled_prop, unmangled_prop_len);
552553
}

0 commit comments

Comments
 (0)