Skip to content

Commit d001009

Browse files
committed
HashContexts are serializable.
* Modify php_hash_ops to contain the algorithm name and serialize and unserialize methods. * Implement __serialize and __unserialize magic methods on HashContext. Note that serialized HashContexts are not necessarily portable between PHP versions or from architecture to architecture. (Most are, but fast SHA3s are not necessarily.) An UnexpectedValueException is thrown when an unsupported serialization is attempted.
1 parent a5e9950 commit d001009

35 files changed

+1232
-31
lines changed

ext/hash/hash.c

Lines changed: 331 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
#include "php_hash.h"
2424
#include "ext/standard/info.h"
2525
#include "ext/standard/file.h"
26+
#include "ext/standard/php_var.h"
27+
#include "ext/spl/spl_exceptions.h"
2628

2729
#include "zend_interfaces.h"
2830
#include "zend_exceptions.h"
31+
#include "zend_smart_str.h"
2932

3033
#include "hash_arginfo.h"
3134

@@ -111,6 +114,195 @@ PHP_HASH_API int php_hash_copy(const void *ops, void *orig_context, void *dest_c
111114
}
112115
/* }}} */
113116

117+
118+
static size_t parse_serialize_spec(const char **specp, size_t *pos, size_t *sz) {
119+
size_t count;
120+
const char *spec = *specp;
121+
if (*spec == 's') {
122+
*sz = 2;
123+
} else if (*spec == 'l') {
124+
*sz = 4;
125+
} else if (*spec == 'q') {
126+
*sz = 8;
127+
} else if (*spec == 'i') {
128+
*sz = sizeof(int);
129+
} else {
130+
*sz = 1;
131+
}
132+
++spec;
133+
if (isdigit((unsigned char) *spec)) {
134+
count = 0;
135+
while (isdigit((unsigned char) *spec)) {
136+
count = 10 * count + *spec - '0';
137+
++spec;
138+
}
139+
} else {
140+
count = 1;
141+
}
142+
*specp = spec;
143+
// alignment
144+
if (*sz > 1 && (*pos & (*sz - 1)) != 0) {
145+
*pos += *sz - (*pos & (*sz - 1));
146+
}
147+
return count;
148+
}
149+
150+
static uint64_t one_from_buffer(size_t sz, const unsigned char *buf) {
151+
if (sz == 2) {
152+
const uint16_t *x = (const uint16_t *) buf;
153+
return *x;
154+
} else if (sz == 4) {
155+
const uint32_t *x = (const uint32_t *) buf;
156+
return *x;
157+
} else if (sz == 8) {
158+
const uint64_t *x = (const uint64_t *) buf;
159+
return *x;
160+
} else {
161+
return *buf;
162+
}
163+
}
164+
165+
static void one_to_buffer(size_t sz, unsigned char *buf, uint64_t val) {
166+
if (sz == 2) {
167+
uint16_t *x = (uint16_t *) buf;
168+
*x = val;
169+
} else if (sz == 4) {
170+
uint32_t *x = (uint32_t *) buf;
171+
*x = val;
172+
} else if (sz == 8) {
173+
uint64_t *x = (uint64_t *) buf;
174+
*x = val;
175+
} else {
176+
*buf = val;
177+
}
178+
}
179+
180+
PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *hash, zend_long *magic, zval *zv, const char *spec) /* {{{ */
181+
{
182+
size_t pos = 0, sz, count;
183+
unsigned char *buf = (unsigned char *) hash->context;
184+
zval tmp;
185+
*magic = 2;
186+
array_init(zv);
187+
while (*spec != '\0' && *spec != '.') {
188+
char specch = *spec;
189+
count = parse_serialize_spec(&spec, &pos, &sz);
190+
if (pos + count * sz > hash->ops->context_size) {
191+
return FAILURE;
192+
}
193+
if (specch == '-') {
194+
pos += count;
195+
} else if (sz == 1 && count > 1) {
196+
ZVAL_STRINGL(&tmp, (char *) buf + pos, count);
197+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
198+
pos += count;
199+
} else {
200+
while (count > 0) {
201+
uint64_t val = one_from_buffer(sz, buf + pos);
202+
pos += sz;
203+
ZVAL_LONG(&tmp, val);
204+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
205+
#if SIZEOF_ZEND_LONG == 4
206+
if (sz == 8) {
207+
ZVAL_LONG(&tmp, val >> 32);
208+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
209+
}
210+
#endif
211+
--count;
212+
}
213+
}
214+
}
215+
if (*spec == '.' && pos != hash->ops->context_size) {
216+
return FAILURE;
217+
}
218+
return SUCCESS;
219+
}
220+
/* }}} */
221+
222+
PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_long magic, const zval *zv, const char *spec) /* {{{ */
223+
{
224+
size_t pos = 0, sz, count, j = 0;
225+
unsigned char *buf = (unsigned char *) hash->context;
226+
zval *elt;
227+
if (magic != 2 || Z_TYPE_P(zv) != IS_ARRAY) {
228+
return FAILURE;
229+
}
230+
while (*spec != '\0' && *spec != '.') {
231+
char specch = *spec;
232+
count = parse_serialize_spec(&spec, &pos, &sz);
233+
if (pos + count * sz > hash->ops->context_size) {
234+
return FAILURE;
235+
}
236+
if (specch == '-') {
237+
pos += count;
238+
} else if (sz == 1 && count > 1) {
239+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
240+
if (!elt || Z_TYPE_P(elt) != IS_STRING || Z_STRLEN_P(elt) != count) {
241+
return FAILURE;
242+
}
243+
++j;
244+
memcpy(buf + pos, Z_STRVAL_P(elt), count);
245+
pos += count;
246+
} else {
247+
while (count > 0) {
248+
uint64_t val;
249+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
250+
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
251+
return FAILURE;
252+
}
253+
++j;
254+
val = zval_get_long(elt);
255+
#if SIZEOF_ZEND_LONG == 4
256+
if (sz == 8) {
257+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
258+
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
259+
return FAILURE;
260+
}
261+
++j;
262+
val += ((uint64_t) zval_get_long(elt)) << 32;
263+
}
264+
#endif
265+
one_to_buffer(sz, buf + pos, val);
266+
pos += sz;
267+
--count;
268+
}
269+
}
270+
}
271+
if (*spec == '.' && pos != hash->ops->context_size) {
272+
return FAILURE;
273+
}
274+
return SUCCESS;
275+
}
276+
/* }}} */
277+
278+
PHP_HASH_API int php_hash_serialize(const php_hashcontext_object *hash, zend_long *magic, zval *zv) /* {{{ */
279+
{
280+
if (hash->ops->serialize_spec) {
281+
return php_hash_serialize_spec(hash, magic, zv, hash->ops->serialize_spec);
282+
} else {
283+
*magic = PHP_HASH_SERIALIZE_MAGIC;
284+
ZVAL_STRINGL(zv, (const char *) hash->context, hash->ops->context_size);
285+
return SUCCESS;
286+
}
287+
}
288+
/* }}} */
289+
290+
PHP_HASH_API int php_hash_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv) /* {{{ */
291+
{
292+
if (hash->ops->serialize_spec) {
293+
return php_hash_unserialize_spec(hash, magic, zv, hash->ops->serialize_spec);
294+
} else {
295+
if (Z_TYPE_P(zv) != IS_STRING
296+
|| Z_STRLEN_P(zv) != hash->ops->context_size
297+
|| magic != PHP_HASH_SERIALIZE_MAGIC) {
298+
return FAILURE;
299+
}
300+
memcpy(hash->context, Z_STRVAL_P(zv), hash->ops->context_size);
301+
return SUCCESS;
302+
}
303+
}
304+
/* }}} */
305+
114306
/* Userspace */
115307

116308
static void php_hash_do_hash(INTERNAL_FUNCTION_PARAMETERS, int isfilename, zend_bool raw_output_default) /* {{{ */
@@ -1170,6 +1362,145 @@ static zend_object *php_hashcontext_clone(zend_object *zobj) {
11701362
}
11711363
/* }}} */
11721364

1365+
/* Serialization format: 5- or 6-element array
1366+
Index 0: hash algorithm (string)
1367+
Index 1: options (long, 0 or PHP_HASH_HMAC)
1368+
Index 2: hash-determined serialization of internal state (mixed, usually string)
1369+
Index 3: magic number defining layout of internal state (long)
1370+
Index 4: properties (array)
1371+
Index 5: key (string, only if PHP_HASH_HMAC)
1372+
1373+
HashContext serializations are not necessarily portable between architectures or
1374+
PHP versions. If the format of a serialized hash context changes, that should
1375+
be reflected in either a different value of `magic` or a different length of
1376+
the serialized context string. A particular hash algorithm can make its
1377+
HashContext serialization portable by parsing different representations in
1378+
its custom `hash_unserialize` method. */
1379+
1380+
/* {{{ proto array HashContext::__serialize()
1381+
Serialize the object */
1382+
PHP_METHOD(HashContext, __serialize)
1383+
{
1384+
zval *object = ZEND_THIS;
1385+
php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(object));
1386+
zend_long magic = 0;
1387+
zval tmp;
1388+
1389+
if (zend_parse_parameters_none() == FAILURE) {
1390+
RETURN_THROWS();
1391+
}
1392+
1393+
array_init(return_value);
1394+
1395+
if (!hash->ops->hash_serialize) {
1396+
goto serialize_failure;
1397+
}
1398+
1399+
ZVAL_STRING(&tmp, hash->ops->algo);
1400+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1401+
1402+
ZVAL_LONG(&tmp, hash->options);
1403+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1404+
1405+
if (hash->ops->hash_serialize(hash, &magic, &tmp) != SUCCESS) {
1406+
goto serialize_failure;
1407+
}
1408+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1409+
1410+
ZVAL_LONG(&tmp, magic);
1411+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1412+
1413+
/* members */
1414+
ZVAL_ARR(&tmp, zend_std_get_properties(&hash->std));
1415+
Z_TRY_ADDREF(tmp);
1416+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1417+
1418+
if (hash->options & PHP_HASH_HMAC) {
1419+
ZVAL_STRINGL(&tmp, (const char *) hash->key, hash->ops->block_size);
1420+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1421+
}
1422+
1423+
return;
1424+
1425+
serialize_failure:
1426+
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "HashContext for algorithm '%s' cannot be serialized", hash->ops->algo);
1427+
RETURN_THROWS();
1428+
}
1429+
/* }}} */
1430+
1431+
/* {{{ proto void HashContext::__unserialize(array serialized)
1432+
* unserialize the object
1433+
*/
1434+
PHP_METHOD(HashContext, __unserialize)
1435+
{
1436+
zval *object = ZEND_THIS;
1437+
php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(object));
1438+
HashTable *data;
1439+
zval *algo_zv, *magic_zv, *options_zv, *hash_zv, *members_zv, *key_zv;
1440+
zend_long magic, options;
1441+
const php_hash_ops *ops;
1442+
1443+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &data) == FAILURE) {
1444+
RETURN_THROWS();
1445+
}
1446+
1447+
if (hash->context) {
1448+
zend_throw_exception(spl_ce_LogicException, "HashContext::__unserialize called on initialized object", 0);
1449+
RETURN_THROWS();
1450+
}
1451+
1452+
algo_zv = zend_hash_index_find(data, 0);
1453+
options_zv = zend_hash_index_find(data, 1);
1454+
hash_zv = zend_hash_index_find(data, 2);
1455+
magic_zv = zend_hash_index_find(data, 3);
1456+
members_zv = zend_hash_index_find(data, 4);
1457+
key_zv = zend_hash_index_find(data, 5);
1458+
1459+
if (!algo_zv || Z_TYPE_P(algo_zv) != IS_STRING
1460+
|| !magic_zv || Z_TYPE_P(magic_zv) != IS_LONG
1461+
|| !options_zv || Z_TYPE_P(options_zv) != IS_LONG
1462+
|| !hash_zv
1463+
|| !members_zv || Z_TYPE_P(members_zv) != IS_ARRAY) {
1464+
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Incomplete or ill-formed serialization data");
1465+
RETURN_THROWS();
1466+
}
1467+
1468+
magic = zval_get_long(magic_zv);
1469+
options = zval_get_long(options_zv);
1470+
1471+
ops = php_hash_fetch_ops(Z_STR_P(algo_zv));
1472+
if (!ops) {
1473+
zend_throw_exception(spl_ce_UnexpectedValueException, "Unknown hash algorithm", 0);
1474+
RETURN_THROWS();
1475+
} else if (!ops->hash_unserialize) {
1476+
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Hash algorithm '%s' cannot be unserialized", ops->algo);
1477+
RETURN_THROWS();
1478+
} else if ((options & PHP_HASH_HMAC)
1479+
&& (!key_zv || Z_TYPE_P(key_zv) != IS_STRING
1480+
|| Z_STRLEN_P(key_zv) != ops->block_size)) {
1481+
zend_throw_exception(spl_ce_UnexpectedValueException, "Incomplete or ill-formed serialization data", 0);
1482+
RETURN_THROWS();
1483+
}
1484+
1485+
hash->ops = ops;
1486+
hash->context = emalloc(ops->context_size);
1487+
ops->hash_init(hash->context);
1488+
hash->options = zval_get_long(options_zv);
1489+
1490+
if (ops->hash_unserialize(hash, magic, hash_zv) != SUCCESS) {
1491+
zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "HashContext for algorithm '%s' cannot be unserialized, format may be non-portable", ops->algo);
1492+
RETURN_THROWS();
1493+
}
1494+
1495+
if (options & PHP_HASH_HMAC) {
1496+
hash->key = (unsigned char *) emalloc(ops->block_size);
1497+
memcpy(hash->key, Z_STRVAL_P(key_zv), ops->block_size);
1498+
}
1499+
1500+
object_properties_load(&hash->std, Z_ARRVAL_P(members_zv));
1501+
}
1502+
/* }}} */
1503+
11731504
/* {{{ PHP_MINIT_FUNCTION
11741505
*/
11751506
PHP_MINIT_FUNCTION(hash)
@@ -1241,8 +1572,6 @@ PHP_MINIT_FUNCTION(hash)
12411572
php_hashcontext_ce = zend_register_internal_class(&ce);
12421573
php_hashcontext_ce->ce_flags |= ZEND_ACC_FINAL;
12431574
php_hashcontext_ce->create_object = php_hashcontext_create;
1244-
php_hashcontext_ce->serialize = zend_class_serialize_deny;
1245-
php_hashcontext_ce->unserialize = zend_class_unserialize_deny;
12461575

12471576
memcpy(&php_hashcontext_handlers, &std_object_handlers,
12481577
sizeof(zend_object_handlers));

ext/hash/hash.stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,8 @@ function mhash(int $hash, string $data, string $key = UNKNOWN): string|false {}
5353
final class HashContext
5454
{
5555
private function __construct() {}
56+
57+
public function __serialize(): array {}
58+
59+
public function __unserialize(array $serialized): void {}
5660
}

ext/hash/hash_adler32.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,14 @@ PHP_HASH_API int PHP_ADLER32Copy(const php_hash_ops *ops, PHP_ADLER32_CTX *orig_
5959
}
6060

6161
const php_hash_ops php_hash_adler32_ops = {
62+
"adler32",
6263
(php_hash_init_func_t) PHP_ADLER32Init,
6364
(php_hash_update_func_t) PHP_ADLER32Update,
6465
(php_hash_final_func_t) PHP_ADLER32Final,
6566
(php_hash_copy_func_t) PHP_ADLER32Copy,
67+
php_hash_serialize,
68+
php_hash_unserialize,
69+
PHP_ADLER32_SPEC,
6670
4, /* what to say here? */
6771
4,
6872
sizeof(PHP_ADLER32_CTX),

0 commit comments

Comments
 (0)