Skip to content

Commit d8eba36

Browse files
committed
Fix object reference key collision in msgpack_var_add
Objects were keyed by a mix of class entry pointer and handle, which is not unique: two objects of different classes could share a key, so one was packed as a back-reference to the other. Key objects by their zend_object address in a separate key space from arrays, and hold a reference to each packed object until packing ends so its address cannot be reused (as ext/standard/var.c does).
1 parent 4a68908 commit d8eba36

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

‎msgpack.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ static void msgpack_init_globals(zend_msgpack_globals *msgpack_globals) /* {{{ *
9292
msgpack_globals->illegal_key_insert = 0;
9393
msgpack_globals->use_str8_serialization = 1;
9494
msgpack_globals->serialize.var_hash = NULL;
95+
msgpack_globals->serialize.var_keep = NULL;
9596
msgpack_globals->serialize.level = 0;
9697
}
9798
/* }}} */

‎msgpack_pack.c‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ static inline int msgpack_var_add(HashTable *var_hash, zval *var, zend_long *var
5555
var_noref = var;
5656
}
5757

58-
if ((Z_TYPE_P(var_noref) == IS_OBJECT) && Z_OBJCE_P(var_noref)) {
59-
p = zend_print_long_to_buf(
60-
id + sizeof(id) - 1,
61-
(((size_t)Z_OBJCE_P(var_noref) << 5)
62-
| ((size_t)Z_OBJCE_P(var_noref) >> (sizeof(long) * 8 - 5))) + (long)Z_OBJ_HANDLE_P(var_noref));
58+
if (Z_TYPE_P(var_noref) == IS_OBJECT) {
59+
/* key objects by their zend_object address; the 'o' prefix keeps
60+
* them apart from the (purely numeric) array keys */
61+
p = zend_print_ulong_to_buf(id + sizeof(id) - 1, (zend_ulong)(zend_uintptr_t)Z_OBJ_P(var_noref));
62+
*--p = 'o';
6363
len = id + sizeof(id) - 1 - p;
6464
} else if (Z_TYPE_P(var_noref) == IS_ARRAY) {
6565
p = zend_print_long_to_buf(id + sizeof(id) - 1, (long)(var_noref));
@@ -81,6 +81,14 @@ static inline int msgpack_var_add(HashTable *var_hash, zval *var, zend_long *var
8181
ZVAL_LONG(&zv, zend_hash_num_elements(var_hash) + 1);
8282
zend_hash_str_add(var_hash, p, len, &zv);
8383

84+
if (Z_TYPE_P(var_noref) == IS_OBJECT) {
85+
/* keep the object alive until packing ends, so that its address
86+
* cannot be reused by another (temporary) object */
87+
ZVAL_OBJ(&zv, Z_OBJ_P(var_noref));
88+
Z_ADDREF(zv);
89+
zend_hash_next_index_insert(MSGPACK_G(serialize).var_keep, &zv);
90+
}
91+
8492
return 1;
8593
}
8694
/* }}} */
@@ -94,6 +102,8 @@ void msgpack_serialize_var_init(msgpack_serialize_data_t *var_hash) /* {{{ */ {
94102
ALLOC_HASHTABLE(*var_hash_ptr);
95103
zend_hash_init(*var_hash_ptr, 10, NULL, NULL, 0);
96104
MSGPACK_G(serialize).var_hash = *var_hash_ptr;
105+
ALLOC_HASHTABLE(MSGPACK_G(serialize).var_keep);
106+
zend_hash_init(MSGPACK_G(serialize).var_keep, 10, NULL, ZVAL_PTR_DTOR, 0);
97107
}
98108
++MSGPACK_G(serialize).level;
99109
}
@@ -106,6 +116,9 @@ void msgpack_serialize_var_destroy(msgpack_serialize_data_t *var_hash) /* {{{ */
106116
if (!MSGPACK_G(serialize).level) {
107117
zend_hash_destroy(*var_hash_ptr);
108118
FREE_HASHTABLE(*var_hash_ptr);
119+
zend_hash_destroy(MSGPACK_G(serialize).var_keep);
120+
FREE_HASHTABLE(MSGPACK_G(serialize).var_keep);
121+
MSGPACK_G(serialize).var_keep = NULL;
109122
}
110123
}
111124
/* }}} */

‎php_msgpack.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ ZEND_BEGIN_MODULE_GLOBALS(msgpack)
2929
zend_bool force_f32;
3030
struct {
3131
void *var_hash;
32+
void *var_keep;
3233
unsigned level;
3334
} serialize;
3435
ZEND_END_MODULE_GLOBALS(msgpack)

‎tests/var_hash_collision.phpt‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
--TEST--
2+
Object reference keys must not collide between objects of different classes
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded("msgpack")) {
6+
exit('skip because msgpack extension is missing');
7+
}
8+
?>
9+
--INI--
10+
memory_limit=1G
11+
--FILE--
12+
<?php
13+
class Item {
14+
public $status;
15+
}
16+
17+
class Status {
18+
}
19+
20+
$status = new Status();
21+
$items = array();
22+
for ($i = 0; $i < 200000; $i++) {
23+
$item = new Item();
24+
$item->status = $status;
25+
$items[] = $item;
26+
}
27+
28+
$unpacked = msgpack_unpack(msgpack_pack($items));
29+
foreach ($unpacked as $idx => $value) {
30+
if (!$value instanceof Item) {
31+
echo "index $idx: expected Item, got " . get_class($value) . "\n";
32+
}
33+
}
34+
35+
// temporary objects created during packing must not be confused with later ones
36+
37+
class Tmp {
38+
public $v;
39+
public function __construct($v) { $this->v = $v; }
40+
public function __serialize(): array { return array(new Tmp2($this->v)); }
41+
public function __unserialize(array $data): void { $this->v = $data[0]->v; }
42+
}
43+
44+
class Tmp2 {
45+
public $v;
46+
public function __construct($v) { $this->v = $v; }
47+
}
48+
49+
$list = array();
50+
for ($i = 0; $i < 100; $i++) {
51+
$list[] = new Tmp($i);
52+
}
53+
$unpacked = msgpack_unpack(msgpack_pack($list));
54+
foreach ($unpacked as $idx => $value) {
55+
if (!$value instanceof Tmp || $value->v !== $idx) {
56+
echo "index $idx: wrong value\n";
57+
}
58+
}
59+
echo "done\n";
60+
?>
61+
--EXPECT--
62+
done

0 commit comments

Comments
 (0)