[php-src] PR #24555: Fix GOST and Snefru digests of 512 MiB or more
| From: | Marti2203 | Date: | Thu, 22 Oct 2026 15:55:39 +0000 |
| Subject: | [php-src] PR #24555: Fix GOST and Snefru digests of 512 MiB or more | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39717@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24555
Author: Marti2203
PHP_GOSTUpdate() keeps the message length in bits as two 32-bit words and updates it
like this:
```c
if ((MAX32 - context->count[0]) < (len * 8)) {
context->count[1]++;
context->count[0] = MAX32 - context->count[0];
context->count[0] = (len * 8) - context->count[0];
} else {
context->count[0] += len * 8;
}
```
When the low word wraps, the new low word is one too large, because MAX32 is 2^32 - 1
where 2^32 is needed. A single update of 2^30 bytes or more (2^33 bits) should carry more than once,
but only one is added to the high word. PHP_SNEFRUUpdate() has the same code and also
computes the bit length of the update as (uint32_t) len * 8.
The length is part of the final block, so gost, gost-crypto and
snefru/snefru256 give wrong digests for every input of 2^29 bytes (512
MiB) or more. This also applies to hash_file() and to incremental
hash_update() calls, because the error occurs in whichever update crosses the boundary.
Smaller inputs are not affected.
### Reproducer
Digests of N bytes of a, compared with RHash 1.4.4 (an independent implementation)
built from source:
| N | algo | RHash | PHP before | PHP after |
|---|---|---|---|---|
| 536870911 | gost | a3464828... | a3464828... | a3464828...
|
| 536870911 | snefru | 0348bb73... | 0348bb73... |
0348bb73... |
| 536870912 | gost | 6c30acc1... | 990c57fe... | 6c30acc1...
|
| 536870912 | snefru | 0842a8e7... | d2727e81... |
0842a8e7... |
| 629145600 | gost | 5e8bdc17... | 985f7997... | 5e8bdc17...
|
The 536870912 values before the fix are the same for one hash() call and for 512
hash_update() calls of 1 MiB. After the fix, a single update of 2^30 + 5 bytes gives
the same digest as 1024 updates of 1 MiB plus one of 5 bytes, for both algorithms. That case
exercises the multiple carry.
### Fix
Compute the new counter with 64-bit arithmetic and store it back in the two words. The context
layout and its serialization are unchanged.
### Test
ext/hash/tests/gost_snefru_512mib.phpt hashes 2^29 bytes with 512
hash_update() calls of 1 MiB for gost and snefru, plus one
hash('snefru', ...) call on a 512 MiB string. The expected digests are from
RHash. The test needs about 512 MiB of memory and takes about 40 s, so like the other large-memory
tests it only runs with RUN_RESOURCE_HEAVY_TESTS set, on 64-bit platforms, and not with
SKIP_SLOW_TESTS. It sets memory_limit=-1.
- Without the fix: fails with 990c57fe... / d2727e81... for all three
lines.
- With the fix: passes (about 41 s). Without RUN_RESOURCE_HEAVY_TESTS, it is skipped.
- ext/hash/tests: 77 passed, 8 skipped (including the existing gost.phpt
and snefru.phpt vectors).
This issue was found by an LLM-Enhanced Static Analysis Engine and verified manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)