Skip to content

Commit 8be3f00

Browse files
committed
Make HashContexts 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.) A ValueError is thrown when an unsupported serialization is attempted. Because of security concerns, HASH_HMAC contexts are not currently serializable; attempting to serialize one throws an error.
1 parent ea7d50b commit 8be3f00

36 files changed

+1251
-44
lines changed

ext/hash/hash.c

Lines changed: 322 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,191 @@ 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, (uint32_t) val);
204+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
205+
if (sz == 8) {
206+
ZVAL_LONG(&tmp, val >> 32);
207+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
208+
}
209+
--count;
210+
}
211+
}
212+
}
213+
if (*spec == '.' && pos != hash->ops->context_size) {
214+
return FAILURE;
215+
}
216+
return SUCCESS;
217+
}
218+
/* }}} */
219+
220+
PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_long magic, const zval *zv, const char *spec) /* {{{ */
221+
{
222+
size_t pos = 0, sz, count, j = 0;
223+
unsigned char *buf = (unsigned char *) hash->context;
224+
zval *elt;
225+
if (magic != 2 || Z_TYPE_P(zv) != IS_ARRAY) {
226+
return FAILURE;
227+
}
228+
while (*spec != '\0' && *spec != '.') {
229+
char specch = *spec;
230+
count = parse_serialize_spec(&spec, &pos, &sz);
231+
if (pos + count * sz > hash->ops->context_size) {
232+
return FAILURE;
233+
}
234+
if (specch == '-') {
235+
pos += count;
236+
} else if (sz == 1 && count > 1) {
237+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
238+
if (!elt || Z_TYPE_P(elt) != IS_STRING || Z_STRLEN_P(elt) != count) {
239+
return FAILURE;
240+
}
241+
++j;
242+
memcpy(buf + pos, Z_STRVAL_P(elt), count);
243+
pos += count;
244+
} else {
245+
while (count > 0) {
246+
uint64_t val;
247+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
248+
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
249+
return FAILURE;
250+
}
251+
++j;
252+
val = (uint32_t) zval_get_long(elt);
253+
if (sz == 8) {
254+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
255+
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
256+
return FAILURE;
257+
}
258+
++j;
259+
val += ((uint64_t) zval_get_long(elt)) << 32;
260+
}
261+
one_to_buffer(sz, buf + pos, val);
262+
pos += sz;
263+
--count;
264+
}
265+
}
266+
}
267+
if (*spec == '.' && pos != hash->ops->context_size) {
268+
return FAILURE;
269+
}
270+
return SUCCESS;
271+
}
272+
/* }}} */
273+
274+
PHP_HASH_API int php_hash_serialize(const php_hashcontext_object *hash, zend_long *magic, zval *zv) /* {{{ */
275+
{
276+
if (hash->ops->serialize_spec) {
277+
return php_hash_serialize_spec(hash, magic, zv, hash->ops->serialize_spec);
278+
} else {
279+
*magic = PHP_HASH_SERIALIZE_MAGIC;
280+
ZVAL_STRINGL(zv, (const char *) hash->context, hash->ops->context_size);
281+
return SUCCESS;
282+
}
283+
}
284+
/* }}} */
285+
286+
PHP_HASH_API int php_hash_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv) /* {{{ */
287+
{
288+
if (hash->ops->serialize_spec) {
289+
return php_hash_unserialize_spec(hash, magic, zv, hash->ops->serialize_spec);
290+
} else {
291+
if (Z_TYPE_P(zv) != IS_STRING
292+
|| Z_STRLEN_P(zv) != hash->ops->context_size
293+
|| magic != PHP_HASH_SERIALIZE_MAGIC) {
294+
return FAILURE;
295+
}
296+
memcpy(hash->context, Z_STRVAL_P(zv), hash->ops->context_size);
297+
return SUCCESS;
298+
}
299+
}
300+
/* }}} */
301+
114302
/* Userspace */
115303

116304
static void php_hash_do_hash(INTERNAL_FUNCTION_PARAMETERS, int isfilename, zend_bool raw_output_default) /* {{{ */
@@ -1170,6 +1358,140 @@ static zend_object *php_hashcontext_clone(zend_object *zobj) {
11701358
}
11711359
/* }}} */
11721360

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

12471567
memcpy(&php_hashcontext_handlers, &std_object_handlers,
12481568
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)