Skip to content

Commit 3da2f86

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 1642f2e commit 3da2f86

36 files changed

+1267
-44
lines changed

ext/hash/hash.c

Lines changed: 324 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,138 @@ 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)
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+
1372+
HashContext serializations are not necessarily portable between architectures or
1373+
PHP versions. If the format of a serialized hash context changes, that should
1374+
be reflected in either a different value of `magic` or a different length of
1375+
the serialized context string. A particular hash algorithm can make its
1376+
HashContext serialization portable by parsing different representations in
1377+
its custom `hash_unserialize` method.
1378+
1379+
Currently HASH_HMAC contexts cannot be serialized, because serializing them
1380+
would require serializing the HMAC key in plaintext. */
1381+
1382+
/* {{{ proto array HashContext::__serialize()
1383+
Serialize the object */
1384+
PHP_METHOD(HashContext, __serialize)
1385+
{
1386+
zval *object = ZEND_THIS;
1387+
php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(object));
1388+
zend_long magic = 0;
1389+
zval tmp;
1390+
1391+
if (zend_parse_parameters_none() == FAILURE) {
1392+
RETURN_THROWS();
1393+
}
1394+
1395+
array_init(return_value);
1396+
1397+
if (!hash->ops->hash_serialize) {
1398+
goto serialize_failure;
1399+
} else if (hash->options & PHP_HASH_HMAC) {
1400+
zend_value_error("HashContext with HASH_HMAC option cannot be serialized");
1401+
RETURN_THROWS();
1402+
}
1403+
1404+
ZVAL_STRING(&tmp, hash->ops->algo);
1405+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1406+
1407+
ZVAL_LONG(&tmp, hash->options);
1408+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1409+
1410+
if (hash->ops->hash_serialize(hash, &magic, &tmp) != SUCCESS) {
1411+
goto serialize_failure;
1412+
}
1413+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1414+
1415+
ZVAL_LONG(&tmp, magic);
1416+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1417+
1418+
/* members */
1419+
ZVAL_ARR(&tmp, zend_std_get_properties(&hash->std));
1420+
Z_TRY_ADDREF(tmp);
1421+
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
1422+
1423+
return;
1424+
1425+
serialize_failure:
1426+
zend_value_error("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;
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+
1458+
if (!algo_zv || Z_TYPE_P(algo_zv) != IS_STRING
1459+
|| !magic_zv || Z_TYPE_P(magic_zv) != IS_LONG
1460+
|| !options_zv || Z_TYPE_P(options_zv) != IS_LONG
1461+
|| !hash_zv
1462+
|| !members_zv || Z_TYPE_P(members_zv) != IS_ARRAY) {
1463+
zend_value_error("Incomplete or ill-formed serialization data");
1464+
RETURN_THROWS();
1465+
}
1466+
1467+
magic = zval_get_long(magic_zv);
1468+
options = zval_get_long(options_zv);
1469+
if (options & PHP_HASH_HMAC) {
1470+
zend_value_error("HashContext with HASH_HMAC option cannot be serialized");
1471+
RETURN_THROWS();
1472+
}
1473+
1474+
ops = php_hash_fetch_ops(Z_STR_P(algo_zv));
1475+
if (!ops) {
1476+
zend_value_error("Unknown hash algorithm");
1477+
RETURN_THROWS();
1478+
} else if (!ops->hash_unserialize) {
1479+
zend_value_error("Hash algorithm '%s' cannot be unserialized", ops->algo);
1480+
RETURN_THROWS();
1481+
}
1482+
1483+
hash->ops = ops;
1484+
hash->context = emalloc(ops->context_size);
1485+
ops->hash_init(hash->context);
1486+
hash->options = options;
1487+
1488+
if (ops->hash_unserialize(hash, magic, hash_zv) != SUCCESS) {
1489+
zend_value_error("HashContext for algorithm '%s' cannot be unserialized, format may be non-portable", ops->algo);
1490+
RETURN_THROWS();
1491+
}
1492+
1493+
object_properties_load(&hash->std, Z_ARRVAL_P(members_zv));
1494+
}
1495+
/* }}} */
1496+
11731497
/* {{{ PHP_MINIT_FUNCTION
11741498
*/
11751499
PHP_MINIT_FUNCTION(hash)
@@ -1241,8 +1565,6 @@ PHP_MINIT_FUNCTION(hash)
12411565
php_hashcontext_ce = zend_register_internal_class(&ce);
12421566
php_hashcontext_ce->ce_flags |= ZEND_ACC_FINAL;
12431567
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;
12461568

12471569
memcpy(&php_hashcontext_handlers, &std_object_handlers,
12481570
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)