Skip to content

Commit 3106e92

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 15d2b65 commit 3106e92

36 files changed

+1277
-44
lines changed

ext/hash/hash.c

Lines changed: 349 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,216 @@ 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+
/* Serialize a hash context according to a `spec` string.
181+
Spec contents:
182+
b[COUNT] -- serialize COUNT bytes
183+
s[COUNT] -- serialize COUNT 16-bit integers
184+
l[COUNT] -- serialize COUNT 32-bit integers
185+
q[COUNT] -- serialize COUNT 64-bit integers
186+
i[COUNT] -- serialize COUNT `int`s
187+
. (must be last character) -- assert that the hash context has exactly
188+
this size
189+
Example: "llllllb64l16." is the spec for an MD5 context: 6 32-bit
190+
integers, followed by 64 bytes, then 16 32-bit integers, and that's
191+
exactly the size of the context.
192+
193+
The serialization result is an array. Each integer is serialized as a
194+
32-bit integer, except that a run of 2 or more bytes is encoded as a
195+
string, and each 64-bit integer is serialized as two 32-bit integers, least
196+
significant bits first. This allows 32-bit and 64-bit architectures to
197+
interchange serialized HashContexts. */
198+
199+
PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *hash, zend_long *magic, zval *zv, const char *spec) /* {{{ */
200+
{
201+
size_t pos = 0, sz, count;
202+
unsigned char *buf = (unsigned char *) hash->context;
203+
zval tmp;
204+
*magic = PHP_HASH_SERIALIZE_MAGIC_SPEC;
205+
array_init(zv);
206+
while (*spec != '\0' && *spec != '.') {
207+
char specch = *spec;
208+
count = parse_serialize_spec(&spec, &pos, &sz);
209+
if (pos + count * sz > hash->ops->context_size) {
210+
return FAILURE;
211+
}
212+
if (specch == '-') {
213+
pos += count;
214+
} else if (sz == 1 && count > 1) {
215+
ZVAL_STRINGL(&tmp, (char *) buf + pos, count);
216+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
217+
pos += count;
218+
} else {
219+
while (count > 0) {
220+
uint64_t val = one_from_buffer(sz, buf + pos);
221+
pos += sz;
222+
ZVAL_LONG(&tmp, (int32_t) val);
223+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
224+
if (sz == 8) {
225+
ZVAL_LONG(&tmp, (int32_t) (val >> 32));
226+
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
227+
}
228+
--count;
229+
}
230+
}
231+
}
232+
if (*spec == '.' && pos != hash->ops->context_size) {
233+
return FAILURE;
234+
}
235+
return SUCCESS;
236+
}
237+
/* }}} */
238+
239+
/* Unserialize a hash context serialized by `php_hash_serialize_spec` with `spec`.
240+
Returns SUCCESS on success and a negative error code on failure.
241+
Codes: FAILURE (-1) == generic failure
242+
-999 == spec wrong size for context
243+
-1000 - POS == problem at byte offset POS */
244+
245+
PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_long magic, const zval *zv, const char *spec) /* {{{ */
246+
{
247+
size_t pos = 0, sz, count, j = 0;
248+
unsigned char *buf = (unsigned char *) hash->context;
249+
zval *elt;
250+
if (magic != PHP_HASH_SERIALIZE_MAGIC_SPEC || Z_TYPE_P(zv) != IS_ARRAY) {
251+
return FAILURE;
252+
}
253+
while (*spec != '\0' && *spec != '.') {
254+
char specch = *spec;
255+
count = parse_serialize_spec(&spec, &pos, &sz);
256+
if (pos + count * sz > hash->ops->context_size) {
257+
return -999;
258+
}
259+
if (specch == '-') {
260+
pos += count;
261+
} else if (sz == 1 && count > 1) {
262+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
263+
if (!elt || Z_TYPE_P(elt) != IS_STRING || Z_STRLEN_P(elt) != count) {
264+
return -1000 - pos;
265+
}
266+
++j;
267+
memcpy(buf + pos, Z_STRVAL_P(elt), count);
268+
pos += count;
269+
} else {
270+
while (count > 0) {
271+
uint64_t val;
272+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
273+
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
274+
return -1000 - pos;
275+
}
276+
++j;
277+
val = (uint32_t) zval_get_long(elt);
278+
if (sz == 8) {
279+
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
280+
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
281+
return -1000 - pos;
282+
}
283+
++j;
284+
val += ((uint64_t) zval_get_long(elt)) << 32;
285+
}
286+
one_to_buffer(sz, buf + pos, val);
287+
pos += sz;
288+
--count;
289+
}
290+
}
291+
}
292+
if (*spec == '.' && pos != hash->ops->context_size) {
293+
return -999;
294+
}
295+
return SUCCESS;
296+
}
297+
/* }}} */
298+
299+
PHP_HASH_API int php_hash_serialize(const php_hashcontext_object *hash, zend_long *magic, zval *zv) /* {{{ */
300+
{
301+
if (hash->ops->serialize_spec) {
302+
return php_hash_serialize_spec(hash, magic, zv, hash->ops->serialize_spec);
303+
} else {
304+
*magic = PHP_HASH_SERIALIZE_MAGIC;
305+
ZVAL_STRINGL(zv, (const char *) hash->context, hash->ops->context_size);
306+
return SUCCESS;
307+
}
308+
}
309+
/* }}} */
310+
311+
PHP_HASH_API int php_hash_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv) /* {{{ */
312+
{
313+
if (hash->ops->serialize_spec) {
314+
return php_hash_unserialize_spec(hash, magic, zv, hash->ops->serialize_spec);
315+
} else {
316+
if (Z_TYPE_P(zv) != IS_STRING
317+
|| Z_STRLEN_P(zv) != hash->ops->context_size
318+
|| magic != PHP_HASH_SERIALIZE_MAGIC) {
319+
return FAILURE;
320+
}
321+
memcpy(hash->context, Z_STRVAL_P(zv), hash->ops->context_size);
322+
return SUCCESS;
323+
}
324+
}
325+
/* }}} */
326+
114327
/* Userspace */
115328

116329
static void php_hash_do_hash(INTERNAL_FUNCTION_PARAMETERS, int isfilename, zend_bool raw_output_default) /* {{{ */
@@ -1170,6 +1383,142 @@ static zend_object *php_hashcontext_clone(zend_object *zobj) {
11701383
}
11711384
/* }}} */
11721385

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

12471594
memcpy(&php_hashcontext_handlers, &std_object_handlers,
12481595
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
}

0 commit comments

Comments
 (0)