Skip to content

Commit

Permalink
Make HashContexts serializable.
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
kohler committed Jun 22, 2020
1 parent 1642f2e commit 3da2f86
Show file tree
Hide file tree
Showing 36 changed files with 1,267 additions and 44 deletions.
326 changes: 324 additions & 2 deletions ext/hash/hash.c
Expand Up @@ -23,9 +23,12 @@
#include "php_hash.h"
#include "ext/standard/info.h"
#include "ext/standard/file.h"
#include "ext/standard/php_var.h"
#include "ext/spl/spl_exceptions.h"

#include "zend_interfaces.h"
#include "zend_exceptions.h"
#include "zend_smart_str.h"

#include "hash_arginfo.h"

Expand Down Expand Up @@ -111,6 +114,195 @@ PHP_HASH_API int php_hash_copy(const void *ops, void *orig_context, void *dest_c
}
/* }}} */


static size_t parse_serialize_spec(const char **specp, size_t *pos, size_t *sz) {
size_t count;
const char *spec = *specp;
if (*spec == 's') {
*sz = 2;
} else if (*spec == 'l') {
*sz = 4;
} else if (*spec == 'q') {
*sz = 8;
} else if (*spec == 'i') {
*sz = sizeof(int);
} else {
*sz = 1;
}
++spec;
if (isdigit((unsigned char) *spec)) {
count = 0;
while (isdigit((unsigned char) *spec)) {
count = 10 * count + *spec - '0';
++spec;
}
} else {
count = 1;
}
*specp = spec;
// alignment
if (*sz > 1 && (*pos & (*sz - 1)) != 0) {
*pos += *sz - (*pos & (*sz - 1));
}
return count;
}

static uint64_t one_from_buffer(size_t sz, const unsigned char *buf) {
if (sz == 2) {
const uint16_t *x = (const uint16_t *) buf;
return *x;
} else if (sz == 4) {
const uint32_t *x = (const uint32_t *) buf;
return *x;
} else if (sz == 8) {
const uint64_t *x = (const uint64_t *) buf;
return *x;
} else {
return *buf;
}
}

static void one_to_buffer(size_t sz, unsigned char *buf, uint64_t val) {
if (sz == 2) {
uint16_t *x = (uint16_t *) buf;
*x = val;
} else if (sz == 4) {
uint32_t *x = (uint32_t *) buf;
*x = val;
} else if (sz == 8) {
uint64_t *x = (uint64_t *) buf;
*x = val;
} else {
*buf = val;
}
}

PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *hash, zend_long *magic, zval *zv, const char *spec) /* {{{ */
{
size_t pos = 0, sz, count;
unsigned char *buf = (unsigned char *) hash->context;
zval tmp;
*magic = 2;
array_init(zv);
while (*spec != '\0' && *spec != '.') {
char specch = *spec;
count = parse_serialize_spec(&spec, &pos, &sz);
if (pos + count * sz > hash->ops->context_size) {
return FAILURE;
}
if (specch == '-') {
pos += count;
} else if (sz == 1 && count > 1) {
ZVAL_STRINGL(&tmp, (char *) buf + pos, count);
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
pos += count;
} else {
while (count > 0) {
uint64_t val = one_from_buffer(sz, buf + pos);
pos += sz;
ZVAL_LONG(&tmp, val);
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
#if SIZEOF_ZEND_LONG == 4
if (sz == 8) {
ZVAL_LONG(&tmp, val >> 32);
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
}
#endif
--count;
}
}
}
if (*spec == '.' && pos != hash->ops->context_size) {
return FAILURE;
}
return SUCCESS;
}
/* }}} */

PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_long magic, const zval *zv, const char *spec) /* {{{ */
{
size_t pos = 0, sz, count, j = 0;
unsigned char *buf = (unsigned char *) hash->context;
zval *elt;
if (magic != 2 || Z_TYPE_P(zv) != IS_ARRAY) {
return FAILURE;
}
while (*spec != '\0' && *spec != '.') {
char specch = *spec;
count = parse_serialize_spec(&spec, &pos, &sz);
if (pos + count * sz > hash->ops->context_size) {
return FAILURE;
}
if (specch == '-') {
pos += count;
} else if (sz == 1 && count > 1) {
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
if (!elt || Z_TYPE_P(elt) != IS_STRING || Z_STRLEN_P(elt) != count) {
return FAILURE;
}
++j;
memcpy(buf + pos, Z_STRVAL_P(elt), count);
pos += count;
} else {
while (count > 0) {
uint64_t val;
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
return FAILURE;
}
++j;
val = zval_get_long(elt);
#if SIZEOF_ZEND_LONG == 4
if (sz == 8) {
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
return FAILURE;
}
++j;
val += ((uint64_t) zval_get_long(elt)) << 32;
}
#endif
one_to_buffer(sz, buf + pos, val);
pos += sz;
--count;
}
}
}
if (*spec == '.' && pos != hash->ops->context_size) {
return FAILURE;
}
return SUCCESS;
}
/* }}} */

PHP_HASH_API int php_hash_serialize(const php_hashcontext_object *hash, zend_long *magic, zval *zv) /* {{{ */
{
if (hash->ops->serialize_spec) {
return php_hash_serialize_spec(hash, magic, zv, hash->ops->serialize_spec);
} else {
*magic = PHP_HASH_SERIALIZE_MAGIC;
ZVAL_STRINGL(zv, (const char *) hash->context, hash->ops->context_size);
return SUCCESS;
}
}
/* }}} */

PHP_HASH_API int php_hash_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv) /* {{{ */
{
if (hash->ops->serialize_spec) {
return php_hash_unserialize_spec(hash, magic, zv, hash->ops->serialize_spec);
} else {
if (Z_TYPE_P(zv) != IS_STRING
|| Z_STRLEN_P(zv) != hash->ops->context_size
|| magic != PHP_HASH_SERIALIZE_MAGIC) {
return FAILURE;
}
memcpy(hash->context, Z_STRVAL_P(zv), hash->ops->context_size);
return SUCCESS;
}
}
/* }}} */

/* Userspace */

static void php_hash_do_hash(INTERNAL_FUNCTION_PARAMETERS, int isfilename, zend_bool raw_output_default) /* {{{ */
Expand Down Expand Up @@ -1170,6 +1362,138 @@ static zend_object *php_hashcontext_clone(zend_object *zobj) {
}
/* }}} */

/* Serialization format: 5- or 6-element array
Index 0: hash algorithm (string)
Index 1: options (long, 0)
Index 2: hash-determined serialization of internal state (mixed, usually string)
Index 3: magic number defining layout of internal state (long)
Index 4: properties (array)
HashContext serializations are not necessarily portable between architectures or
PHP versions. If the format of a serialized hash context changes, that should
be reflected in either a different value of `magic` or a different length of
the serialized context string. A particular hash algorithm can make its
HashContext serialization portable by parsing different representations in
its custom `hash_unserialize` method.
Currently HASH_HMAC contexts cannot be serialized, because serializing them
would require serializing the HMAC key in plaintext. */

/* {{{ proto array HashContext::__serialize()
Serialize the object */
PHP_METHOD(HashContext, __serialize)
{
zval *object = ZEND_THIS;
php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(object));
zend_long magic = 0;
zval tmp;

if (zend_parse_parameters_none() == FAILURE) {
RETURN_THROWS();
}

array_init(return_value);

if (!hash->ops->hash_serialize) {
goto serialize_failure;
} else if (hash->options & PHP_HASH_HMAC) {
zend_value_error("HashContext with HASH_HMAC option cannot be serialized");
RETURN_THROWS();
}

ZVAL_STRING(&tmp, hash->ops->algo);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);

ZVAL_LONG(&tmp, hash->options);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);

if (hash->ops->hash_serialize(hash, &magic, &tmp) != SUCCESS) {
goto serialize_failure;
}
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);

ZVAL_LONG(&tmp, magic);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);

/* members */
ZVAL_ARR(&tmp, zend_std_get_properties(&hash->std));
Z_TRY_ADDREF(tmp);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);

return;

serialize_failure:
zend_value_error("HashContext for algorithm '%s' cannot be serialized", hash->ops->algo);
RETURN_THROWS();
}
/* }}} */

/* {{{ proto void HashContext::__unserialize(array serialized)
* unserialize the object
*/
PHP_METHOD(HashContext, __unserialize)
{
zval *object = ZEND_THIS;
php_hashcontext_object *hash = php_hashcontext_from_object(Z_OBJ_P(object));
HashTable *data;
zval *algo_zv, *magic_zv, *options_zv, *hash_zv, *members_zv;
zend_long magic, options;
const php_hash_ops *ops;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &data) == FAILURE) {
RETURN_THROWS();
}

if (hash->context) {
zend_throw_exception(spl_ce_LogicException, "HashContext::__unserialize called on initialized object", 0);
RETURN_THROWS();
}

algo_zv = zend_hash_index_find(data, 0);
options_zv = zend_hash_index_find(data, 1);
hash_zv = zend_hash_index_find(data, 2);
magic_zv = zend_hash_index_find(data, 3);
members_zv = zend_hash_index_find(data, 4);

if (!algo_zv || Z_TYPE_P(algo_zv) != IS_STRING
|| !magic_zv || Z_TYPE_P(magic_zv) != IS_LONG
|| !options_zv || Z_TYPE_P(options_zv) != IS_LONG
|| !hash_zv
|| !members_zv || Z_TYPE_P(members_zv) != IS_ARRAY) {
zend_value_error("Incomplete or ill-formed serialization data");
RETURN_THROWS();
}

magic = zval_get_long(magic_zv);
options = zval_get_long(options_zv);
if (options & PHP_HASH_HMAC) {
zend_value_error("HashContext with HASH_HMAC option cannot be serialized");
RETURN_THROWS();
}

ops = php_hash_fetch_ops(Z_STR_P(algo_zv));
if (!ops) {
zend_value_error("Unknown hash algorithm");
RETURN_THROWS();
} else if (!ops->hash_unserialize) {
zend_value_error("Hash algorithm '%s' cannot be unserialized", ops->algo);
RETURN_THROWS();
}

hash->ops = ops;
hash->context = emalloc(ops->context_size);
ops->hash_init(hash->context);
hash->options = options;

if (ops->hash_unserialize(hash, magic, hash_zv) != SUCCESS) {
zend_value_error("HashContext for algorithm '%s' cannot be unserialized, format may be non-portable", ops->algo);
RETURN_THROWS();
}

object_properties_load(&hash->std, Z_ARRVAL_P(members_zv));
}
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
*/
PHP_MINIT_FUNCTION(hash)
Expand Down Expand Up @@ -1241,8 +1565,6 @@ PHP_MINIT_FUNCTION(hash)
php_hashcontext_ce = zend_register_internal_class(&ce);
php_hashcontext_ce->ce_flags |= ZEND_ACC_FINAL;
php_hashcontext_ce->create_object = php_hashcontext_create;
php_hashcontext_ce->serialize = zend_class_serialize_deny;
php_hashcontext_ce->unserialize = zend_class_unserialize_deny;

memcpy(&php_hashcontext_handlers, &std_object_handlers,
sizeof(zend_object_handlers));
Expand Down
4 changes: 4 additions & 0 deletions ext/hash/hash.stub.php
Expand Up @@ -53,4 +53,8 @@ function mhash(int $hash, string $data, string $key = UNKNOWN): string|false {}
final class HashContext
{
private function __construct() {}

public function __serialize(): array {}

public function __unserialize(array $serialized): void {}
}
4 changes: 4 additions & 0 deletions ext/hash/hash_adler32.c
Expand Up @@ -59,10 +59,14 @@ PHP_HASH_API int PHP_ADLER32Copy(const php_hash_ops *ops, PHP_ADLER32_CTX *orig_
}

const php_hash_ops php_hash_adler32_ops = {
"adler32",
(php_hash_init_func_t) PHP_ADLER32Init,
(php_hash_update_func_t) PHP_ADLER32Update,
(php_hash_final_func_t) PHP_ADLER32Final,
(php_hash_copy_func_t) PHP_ADLER32Copy,
php_hash_serialize,
php_hash_unserialize,
PHP_ADLER32_SPEC,
4, /* what to say here? */
4,
sizeof(PHP_ADLER32_CTX),
Expand Down

0 comments on commit 3da2f86

Please sign in to comment.