Skip to content

Fix buffer overflow in hash_pbkdf2() with a large output length - #23380

Open
lazerg wants to merge 1 commit into
php:PHP-8.4from
lazerg:fix/issue-23377-pbkdf2-length-overflow
Open

Fix buffer overflow in hash_pbkdf2() with a large output length#23380
lazerg wants to merge 1 commit into
php:PHP-8.4from
lazerg:fix/issue-23377-pbkdf2-length-overflow

Conversation

@lazerg

@lazerg lazerg commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

hash_pbkdf2() sizes the digest buffer with ceil((float) length / 2.0) and the block count with ceil((float) digest_length / (float) ops->digest_size). A float carries 24 bits of mantissa, so past 2^24 the conversion rounds and both counts come out wrong: rounding up makes zend_bin2hex() write past the end of the return string, rounding down leaves the tail of the string uninitialized. hash_pbkdf2('md5', 'password', 'salt', 1, 268435473) writes 8 bytes out of bounds under ASAN.

Both counts are exact in integer arithmetic, so this replaces them with round-up divisions. <math.h> had no other user in the file.

This came out of #23377. The odd length case reported there is fine on its own, as Sjord noted before closing it: zend_string_alloc(length) reserves length + 1 bytes and 2 * ceil(length / 2) == length + 1, so the extra hexit lands on the terminator byte that gets overwritten a line later. The float conversion on those same two lines is a real overflow though.

@lazerg
lazerg force-pushed the fix/issue-23377-pbkdf2-length-overflow branch from 514c90e to 95e52bd Compare August 19, 2026 13:00
@lazerg lazerg changed the title Fix GH-23377: Buffer overflow in hash_pbkdf2() with a large output length Fix buffer overflow in hash_pbkdf2() with a large output length Aug 19, 2026
@lazerg

lazerg commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

#23377 was closed since the odd-length case turned out fine. This PR fixes something else though, a float-precision overflow that causes a real out-of-bounds write for large output lengths. Reproduced it under ASAN. Still worth a review. @LamentXU123, mind taking a look when you get a chance?

Comment thread ext/hash/hash.c
}

loops = (zend_long) ceil((float) digest_length / (float) ops->digest_size);
loops = (digest_length + ops->digest_size - 1) / ops->digest_size;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this gives a compiler warning (if I enable all warnings):

ext/hash/hash.c:1048:17: warning: conversion to ‘zend_long’ {aka ‘long int’} from ‘long unsigned int’ may change the sign of the result [-Wsign-conversion]
 1048 |         loops = (digest_length + ops->digest_size - 1) / ops->digest_size;
      |                 ^

It is not an actual problem, so I think it's fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it cannot bite here. digest_length is always at least 1 at that point: a negative length is rejected earlier, and 0 is replaced by the digest size. So the unsigned arithmetic cannot wrap, and the quotient always fits back into a zend_long. php-src does not build with -Wsign-conversion, so I left the line as it is. I can add an explicit cast if you would rather not see the warning.

@Sjord

Sjord commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Looks good to me. Thanks for looking into the issue I made.

@LamentXU123
LamentXU123 requested a review from Girgias August 22, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants