Skip to content

Commit ac607dc

Browse files
committed
Audit hash unserialize functions to prevent memory errors.
Some hash internal states have logical pointers into a buffer, or sponge, that absorbs input provided in bytes rather than chunks. The unserialize functions for these hash functions must validate that the logical pointers are all within bounds, lest future hash operations cause out-of-bounds memory accesses. All hash functions were audited for this kind of problem. Sponge/buffer positions appear to be the only values that can cause memory errors after unserialization. * Adler32, CRC32, FNV, joaat: simple state, no buffer positions * Gost, MD2, SHA3, Snefru, Tiger, Whirlpool: buffer positions must be validated * MD4, MD5, SHA1, SHA2, haval, ripemd: buffer positions encoded bitwise, forced to within bounds on use; no need to validate
1 parent fd6ffc8 commit ac607dc

File tree

10 files changed

+177
-57
lines changed

10 files changed

+177
-57
lines changed

ext/hash/hash.c

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,25 @@ static inline size_t align_to(size_t pos, size_t alignment) {
129129
return pos + (offset ? alignment - offset : 0);
130130
}
131131

132-
static size_t parse_serialize_spec(const char **specp, size_t *pos, size_t *sz,
133-
size_t *max_alignment) {
132+
static size_t parse_serialize_spec(
133+
const char **specp, size_t *pos, size_t *sz, size_t *max_alignment) {
134134
size_t count, alignment;
135135
const char *spec = *specp;
136136
/* parse size */
137-
if (*spec == 's') {
137+
if (*spec == 's' || *spec == 'S') {
138138
*sz = 2;
139139
alignment = __alignof__(uint16_t); /* usually 2 */
140-
} else if (*spec == 'l') {
140+
} else if (*spec == 'l' || *spec == 'L') {
141141
*sz = 4;
142142
alignment = __alignof__(uint32_t); /* usually 4 */
143-
} else if (*spec == 'q') {
143+
} else if (*spec == 'q' || *spec == 'Q') {
144144
*sz = 8;
145145
alignment = __alignof__(uint64_t); /* usually 8 */
146-
} else if (*spec == 'i') {
146+
} else if (*spec == 'i' || *spec == 'I') {
147147
*sz = sizeof(int);
148148
alignment = __alignof__(int); /* usually 4 */
149149
} else {
150+
ZEND_ASSERT(*spec == 'b' || *spec == 'B');
150151
*sz = 1;
151152
alignment = 1;
152153
}
@@ -179,6 +180,7 @@ static uint64_t one_from_buffer(size_t sz, const unsigned char *buf) {
179180
const uint64_t *x = (const uint64_t *) buf;
180181
return *x;
181182
} else {
183+
ZEND_ASSERT(sz == 1);
182184
return *buf;
183185
}
184186
}
@@ -194,6 +196,7 @@ static void one_to_buffer(size_t sz, unsigned char *buf, uint64_t val) {
194196
uint64_t *x = (uint64_t *) buf;
195197
*x = val;
196198
} else {
199+
ZEND_ASSERT(sz == 1);
197200
*buf = val;
198201
}
199202
}
@@ -205,7 +208,8 @@ static void one_to_buffer(size_t sz, unsigned char *buf, uint64_t val) {
205208
l[COUNT] -- serialize COUNT 32-bit integers
206209
q[COUNT] -- serialize COUNT 64-bit integers
207210
i[COUNT] -- serialize COUNT `int`s
208-
-[COUNT] -- skip COUNT bytes
211+
B[COUNT] -- skip COUNT bytes
212+
S[COUNT], L[COUNT], etc. -- uppercase versions skip instead of read
209213
. (must be last character) -- assert that the hash context has exactly
210214
this size
211215
Example: "llllllb64l16." is the spec for an MD5 context: 6 32-bit
@@ -218,21 +222,20 @@ static void one_to_buffer(size_t sz, unsigned char *buf, uint64_t val) {
218222
significant bits first. This allows 32-bit and 64-bit architectures to
219223
interchange serialized HashContexts. */
220224

221-
PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *hash, zend_long *magic, zval *zv, const char *spec) /* {{{ */
225+
PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *hash, zval *zv, const char *spec) /* {{{ */
222226
{
223227
size_t pos = 0, max_alignment = 1, sz, count;
224228
unsigned char *buf = (unsigned char *) hash->context;
225229
zval tmp;
226-
*magic = PHP_HASH_SERIALIZE_MAGIC_SPEC;
227230
array_init(zv);
228231
while (*spec != '\0' && *spec != '.') {
229-
char specch = *spec;
232+
char spec_ch = *spec;
230233
count = parse_serialize_spec(&spec, &pos, &sz, &max_alignment);
231234
if (pos + count * sz > hash->ops->context_size) {
232235
return FAILURE;
233236
}
234-
if (specch == '-') {
235-
pos += count;
237+
if (isupper((unsigned char) spec_ch)) {
238+
pos += count * sz;
236239
} else if (sz == 1 && count > 1) {
237240
ZVAL_STRINGL(&tmp, (char *) buf + pos, count);
238241
zend_hash_next_index_insert(Z_ARRVAL_P(zv), &tmp);
@@ -264,22 +267,22 @@ PHP_HASH_API int php_hash_serialize_spec(const php_hashcontext_object *hash, zen
264267
-999 == spec wrong size for context
265268
-1000 - POS == problem at byte offset POS */
266269

267-
PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_long magic, const zval *zv, const char *spec) /* {{{ */
270+
PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, const zval *zv, const char *spec) /* {{{ */
268271
{
269272
size_t pos = 0, max_alignment = 1, sz, count, j = 0;
270273
unsigned char *buf = (unsigned char *) hash->context;
271274
zval *elt;
272-
if (magic != PHP_HASH_SERIALIZE_MAGIC_SPEC || Z_TYPE_P(zv) != IS_ARRAY) {
275+
if (Z_TYPE_P(zv) != IS_ARRAY) {
273276
return FAILURE;
274277
}
275278
while (*spec != '\0' && *spec != '.') {
276-
char specch = *spec;
279+
char spec_ch = *spec;
277280
count = parse_serialize_spec(&spec, &pos, &sz, &max_alignment);
278281
if (pos + count * sz > hash->ops->context_size) {
279282
return -999;
280283
}
281-
if (specch == '-') {
282-
pos += count;
284+
if (isupper((unsigned char) spec_ch)) {
285+
pos += count * sz;
283286
} else if (sz == 1 && count > 1) {
284287
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
285288
if (!elt || Z_TYPE_P(elt) != IS_STRING || Z_STRLEN_P(elt) != count) {
@@ -296,14 +299,14 @@ PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_lo
296299
return -1000 - pos;
297300
}
298301
++j;
299-
val = (uint32_t) zval_get_long(elt);
302+
val = (uint32_t) Z_LVAL_P(elt);
300303
if (sz == 8) {
301304
elt = zend_hash_index_find(Z_ARRVAL_P(zv), j);
302305
if (!elt || Z_TYPE_P(elt) != IS_LONG) {
303306
return -1000 - pos;
304307
}
305308
++j;
306-
val += ((uint64_t) zval_get_long(elt)) << 32;
309+
val += ((uint64_t) Z_LVAL_P(elt)) << 32;
307310
}
308311
one_to_buffer(sz, buf + pos, val);
309312
pos += sz;
@@ -321,27 +324,21 @@ PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, zend_lo
321324
PHP_HASH_API int php_hash_serialize(const php_hashcontext_object *hash, zend_long *magic, zval *zv) /* {{{ */
322325
{
323326
if (hash->ops->serialize_spec) {
324-
return php_hash_serialize_spec(hash, magic, zv, hash->ops->serialize_spec);
327+
*magic = PHP_HASH_SERIALIZE_MAGIC_SPEC;
328+
return php_hash_serialize_spec(hash, zv, hash->ops->serialize_spec);
325329
} else {
326-
*magic = PHP_HASH_SERIALIZE_MAGIC;
327-
ZVAL_STRINGL(zv, (const char *) hash->context, hash->ops->context_size);
328-
return SUCCESS;
330+
return FAILURE;
329331
}
330332
}
331333
/* }}} */
332334

333335
PHP_HASH_API int php_hash_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv) /* {{{ */
334336
{
335-
if (hash->ops->serialize_spec) {
336-
return php_hash_unserialize_spec(hash, magic, zv, hash->ops->serialize_spec);
337+
if (hash->ops->serialize_spec
338+
&& magic == PHP_HASH_SERIALIZE_MAGIC_SPEC) {
339+
return php_hash_unserialize_spec(hash, zv, hash->ops->serialize_spec);
337340
} else {
338-
if (Z_TYPE_P(zv) != IS_STRING
339-
|| Z_STRLEN_P(zv) != hash->ops->context_size
340-
|| magic != PHP_HASH_SERIALIZE_MAGIC) {
341-
return FAILURE;
342-
}
343-
memcpy(hash->context, Z_STRVAL_P(zv), hash->ops->context_size);
344-
return SUCCESS;
341+
return FAILURE;
345342
}
346343
}
347344
/* }}} */
@@ -1466,7 +1463,7 @@ PHP_METHOD(HashContext, __serialize)
14661463
return;
14671464

14681465
serialize_failure:
1469-
zend_value_error("HashContext for algorithm '%s' cannot be serialized", hash->ops->algo);
1466+
zend_value_error("HashContext for algorithm \"%s\" cannot be serialized", hash->ops->algo);
14701467
RETURN_THROWS();
14711468
}
14721469
/* }}} */
@@ -1508,8 +1505,8 @@ PHP_METHOD(HashContext, __unserialize)
15081505
RETURN_THROWS();
15091506
}
15101507

1511-
magic = zval_get_long(magic_zv);
1512-
options = zval_get_long(options_zv);
1508+
magic = Z_LVAL_P(magic_zv);
1509+
options = Z_LVAL_P(options_zv);
15131510
if (options & PHP_HASH_HMAC) {
15141511
zend_value_error("HashContext with HASH_HMAC option cannot be serialized");
15151512
RETURN_THROWS();
@@ -1520,7 +1517,7 @@ PHP_METHOD(HashContext, __unserialize)
15201517
zend_value_error("Unknown hash algorithm");
15211518
RETURN_THROWS();
15221519
} else if (!ops->hash_unserialize) {
1523-
zend_value_error("Hash algorithm '%s' cannot be unserialized", ops->algo);
1520+
zend_value_error("Hash algorithm \"%s\" cannot be unserialized", ops->algo);
15241521
RETURN_THROWS();
15251522
}
15261523

@@ -1531,7 +1528,7 @@ PHP_METHOD(HashContext, __unserialize)
15311528

15321529
unserialize_result = ops->hash_unserialize(hash, magic, hash_zv);
15331530
if (unserialize_result != SUCCESS) {
1534-
zend_value_error("HashContext for algorithm '%s' cannot be unserialized, format may be non-portable (code %d)", ops->algo, unserialize_result);
1531+
zend_value_error("HashContext for algorithm \"%s\" cannot be unserialized, format may be non-portable (code %d)", ops->algo, unserialize_result);
15351532
/* Free internally allocated resources */
15361533
php_hashcontext_dtor(Z_OBJ_P(object));
15371534
RETURN_THROWS();

ext/hash/hash_gost.c

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,27 @@ PHP_HASH_API void PHP_GOSTFinal(unsigned char digest[32], PHP_GOST_CTX *context)
304304
ZEND_SECURE_ZERO(context, sizeof(*context));
305305
}
306306

307+
static int php_gost_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv)
308+
{
309+
PHP_GOST_CTX *ctx = (PHP_GOST_CTX *) hash->context;
310+
int r = FAILURE;
311+
if (magic == PHP_HASH_SERIALIZE_MAGIC_SPEC
312+
&& (r = php_hash_unserialize_spec(hash, zv, PHP_GOST_SPEC)) == SUCCESS
313+
&& ctx->length < sizeof(ctx->buffer)) {
314+
return SUCCESS;
315+
} else {
316+
return r != SUCCESS ? r : -2000;
317+
}
318+
}
319+
307320
const php_hash_ops php_hash_gost_ops = {
308321
"gost",
309322
(php_hash_init_func_t) PHP_GOSTInit,
310323
(php_hash_update_func_t) PHP_GOSTUpdate,
311324
(php_hash_final_func_t) PHP_GOSTFinal,
312325
php_hash_copy,
313326
php_hash_serialize,
314-
php_hash_unserialize,
327+
php_gost_unserialize,
315328
PHP_GOST_SPEC,
316329
32,
317330
32,
@@ -326,7 +339,7 @@ const php_hash_ops php_hash_gost_crypto_ops = {
326339
(php_hash_final_func_t) PHP_GOSTFinal,
327340
php_hash_copy,
328341
php_hash_serialize,
329-
php_hash_unserialize,
342+
php_gost_unserialize,
330343
PHP_GOST_SPEC,
331344
32,
332345
32,

ext/hash/hash_md.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ const php_hash_ops php_hash_md4_ops = {
4747
1
4848
};
4949

50+
static int php_md2_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv);
51+
5052
const php_hash_ops php_hash_md2_ops = {
5153
"md2",
5254
(php_hash_init_func_t) PHP_MD2Init,
5355
(php_hash_update_func_t) PHP_MD2Update,
5456
(php_hash_final_func_t) PHP_MD2Final,
5557
php_hash_copy,
5658
php_hash_serialize,
57-
php_hash_unserialize,
59+
php_md2_unserialize,
5860
PHP_MD2_SPEC,
5961
16,
6062
16,
@@ -352,3 +354,16 @@ PHP_HASH_API void PHP_MD2Final(unsigned char output[16], PHP_MD2_CTX *context)
352354

353355
memcpy(output, context->state, 16);
354356
}
357+
358+
static int php_md2_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv)
359+
{
360+
PHP_MD2_CTX *ctx = (PHP_MD2_CTX *) hash->context;
361+
int r = FAILURE;
362+
if (magic == PHP_HASH_SERIALIZE_MAGIC_SPEC
363+
&& (r = php_hash_unserialize_spec(hash, zv, PHP_MD2_SPEC)) == SUCCESS
364+
&& (unsigned char) ctx->in_buffer < sizeof(ctx->buffer)) {
365+
return SUCCESS;
366+
} else {
367+
return r != SUCCESS ? r : -2000;
368+
}
369+
}

ext/hash/hash_sha3.c

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ static void PHP_SHA3_Final(unsigned char* digest,
201201
ZEND_SECURE_ZERO(ctx, sizeof(PHP_SHA3_CTX));
202202
}
203203

204+
static int php_sha3_unserialize(php_hashcontext_object *hash,
205+
zend_long magic,
206+
const zval *zv,
207+
size_t block_size)
208+
{
209+
PHP_SHA3_CTX *ctx = (PHP_SHA3_CTX *) hash->context;
210+
int r = FAILURE;
211+
if (magic == PHP_HASH_SERIALIZE_MAGIC_SPEC
212+
&& (r = php_hash_unserialize_spec(hash, zv, PHP_SHA3_SPEC)) == SUCCESS
213+
&& ctx->pos < block_size) {
214+
return SUCCESS;
215+
} else {
216+
return r != SUCCESS ? r : -2000;
217+
}
218+
}
219+
204220
// ==========================================================================
205221

206222
#define DECLARE_SHA3_OPS(bits) \
@@ -219,14 +235,19 @@ void PHP_SHA3##bits##Final(unsigned char* digest, \
219235
(1600 - (2 * bits)) >> 3, \
220236
bits >> 3); \
221237
} \
238+
static int php_sha3##bits##_unserialize(php_hashcontext_object *hash, \
239+
zend_long magic, \
240+
const zval *zv) { \
241+
return php_sha3_unserialize(hash, magic, zv, (1600 - (2 * bits)) >> 3); \
242+
} \
222243
const php_hash_ops php_hash_sha3_##bits##_ops = { \
223244
"sha3-" #bits, \
224245
(php_hash_init_func_t) PHP_SHA3##bits##Init, \
225246
(php_hash_update_func_t) PHP_SHA3##bits##Update, \
226247
(php_hash_final_func_t) PHP_SHA3##bits##Final, \
227248
php_hash_copy, \
228249
php_hash_serialize, \
229-
php_hash_unserialize, \
250+
php_sha3##bits##_unserialize, \
230251
PHP_SHA3_SPEC, \
231252
bits >> 3, \
232253
(1600 - (2 * bits)) >> 3, \
@@ -241,6 +262,45 @@ const php_hash_ops php_hash_sha3_##bits##_ops = { \
241262
#define SUCCESS SHA3_SUCCESS /* Avoid conflict between KeccacHash.h and zend_types.h */
242263
#include "KeccakHash.h"
243264

265+
/* KECCAK SERIALIZATION
266+
267+
Keccak_HashInstance consists of:
268+
KeccakWidth1600_SpongeInstance {
269+
unsigned char state[200];
270+
unsigned int rate; -- fixed for digest size
271+
unsigned int byteIOIndex; -- in range [0, rate/8)
272+
int squeezing; -- 0 normally, 1 only during finalize
273+
} sponge;
274+
unsigned int fixedOutputLength; -- fixed for digest size
275+
unsigned char delimitedSuffix; -- fixed for digest size
276+
277+
NB If the external sha3/ library is updated, the serialization code
278+
may need to be updated.
279+
280+
We assume the simpler SHA3 code's serialization states is not
281+
interchangeable with Keccak. */
282+
283+
#define PHP_HASH_SERIALIZE_MAGIC_KECCAK 100
284+
#define PHP_KECCAK_SPEC "b200IiIIB"
285+
286+
static int php_keccak_serialize(const php_hashcontext_object *hash, zend_long *magic, zval *zv)
287+
{
288+
*magic = PHP_HASH_SERIALIZE_MAGIC_KECCAK;
289+
return php_hash_serialize_spec(hash, zv, PHP_KECCAK_SPEC);
290+
}
291+
292+
static int php_keccak_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv)
293+
{
294+
Keccak_HashInstance *ctx = (Keccak_HashInstance *) hash->context;
295+
int r = FAILURE;
296+
if (magic == PHP_HASH_SERIALIZE_MAGIC_KECCAK
297+
&& (r = php_hash_unserialize_spec(hash, zv, PHP_KECCAK_SPEC)) == SUCCESS
298+
&& ctx->sponge.byteIOIndex < ctx->sponge.rate / 8) {
299+
return SUCCESS;
300+
} else {
301+
return r != SUCCESS ? r : -2000;
302+
}
303+
}
244304

245305
// ==========================================================================
246306

@@ -264,9 +324,9 @@ const php_hash_ops php_hash_sha3_##bits##_ops = { \
264324
(php_hash_update_func_t) PHP_SHA3##bits##Update, \
265325
(php_hash_final_func_t) PHP_SHA3##bits##Final, \
266326
php_hash_copy, \
267-
php_hash_serialize, \
268-
php_hash_unserialize, \
269-
NULL, \
327+
php_keccak_serialize, \
328+
php_keccak_unserialize, \
329+
PHP_KECCAK_SPEC, \
270330
bits >> 3, \
271331
(1600 - (2 * bits)) >> 3, \
272332
sizeof(PHP_SHA3_CTX), \

0 commit comments

Comments
 (0)