Skip to content

Commit b9b2df8

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 b9b2df8

2 files changed

Lines changed: 192 additions & 1 deletion

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
// Test 7: Shadowed private properties — alias must not cross-contaminate
103+
// (Addresses concern from GH-18542: \0Alias\0prop and \0Parent\0prop refer
104+
// to different private properties at different inheritance levels)
105+
class Base {
106+
public function __construct(
107+
private string $prop = ''
108+
) {}
109+
public function getBaseProp(): string { return $this->prop; }
110+
}
111+
112+
class Child extends Base {
113+
public function __construct(
114+
private string $prop = '',
115+
string $baseProp = ''
116+
) {
117+
parent::__construct($baseProp);
118+
}
119+
public function getChildProp(): string { return $this->prop; }
120+
}
121+
class_alias(Child::class, 'ChildAlias');
122+
123+
// Serialize with alias name for Child's prop, canonical for Base's prop.
124+
// These are TWO SEPARATE private properties that must not be confused.
125+
$serialized7 = 'O:10:"ChildAlias":2:{s:16:"' . "\0ChildAlias\0" . 'prop";s:5:"child";s:10:"' . "\0Base\0" . 'prop";s:4:"base";}';
126+
$obj7 = unserialize($serialized7);
127+
var_dump($obj7 instanceof Child);
128+
var_dump($obj7->getChildProp());
129+
var_dump($obj7->getBaseProp());
130+
131+
// Test 8: IMSoP's scenario from GH-18542 — alias and canonical name for
132+
// the SAME private property in a single class (no inheritance).
133+
// Both \0Bravo\0value and \0BravoAlias\0value refer to the same declared property.
134+
// The fix resolves both correctly; last one in serialized data wins.
135+
class Bravo {
136+
public function __construct(
137+
private string $value = ''
138+
) {}
139+
public function getValue(): string { return $this->value; }
140+
}
141+
class_alias(Bravo::class, 'BravoAlias');
142+
143+
// Only alias-mangled name
144+
$serialized8 = 'O:10:"BravoAlias":1:{s:17:"' . "\0BravoAlias\0" . 'value";s:5:"alias";}';
145+
$obj8 = unserialize($serialized8);
146+
var_dump($obj8 instanceof Bravo);
147+
var_dump($obj8->getValue());
148+
149+
// Only canonical name
150+
$serialized8b = 'O:10:"BravoAlias":1:{s:12:"' . "\0Bravo\0" . 'value";s:9:"canonical";}';
151+
$obj8b = unserialize($serialized8b);
152+
var_dump($obj8b instanceof Bravo);
153+
var_dump($obj8b->getValue());
154+
155+
// Both alias AND canonical in same payload — last write wins, no dynamic property
156+
$serialized8c = 'O:10:"BravoAlias":2:{s:12:"' . "\0Bravo\0" . 'value";s:5:"first";s:17:"' . "\0BravoAlias\0" . 'value";s:4:"last";}';
157+
$obj8c = unserialize($serialized8c);
158+
var_dump($obj8c instanceof Bravo);
159+
var_dump($obj8c->getValue());
160+
161+
echo "Done\n";
162+
?>
163+
--EXPECT--
164+
bool(true)
165+
int(42)
166+
bool(true)
167+
int(99)
168+
bool(true)
169+
int(77)
170+
bool(true)
171+
int(10)
172+
int(20)
173+
bool(true)
174+
int(1)
175+
string(3) "abc"
176+
bool(true)
177+
bool(true)
178+
int(5)
179+
string(2) "hi"
180+
bool(false)
181+
bool(true)
182+
string(5) "child"
183+
string(4) "base"
184+
bool(true)
185+
string(5) "alias"
186+
bool(true)
187+
string(9) "canonical"
188+
bool(true)
189+
string(4) "last"
190+
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)