[php-src] Issue #21731: ext/random: Random\Engine\Xoshiro256StarStar::__unserialize() accepts all-zero state
[email protected] (iliaal)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/21731
Author: iliaal
## Description
`Random\Engine\Xoshiro256StarStar::__construct()` rejects a seed that would leave the internal state all zero, because xoshiro256** with zero state produces `0` on every call forever:
~~~c
if (UNEXPECTED(t[0] == 0 && t[1] == 0 && t[2] == 0 && t[3] == 0)) {
zend_argument_value_error(1, "must not consist entirely of NUL bytes");
RETURN_THROWS();
}
~~~
`__unserialize()` doesn't check the same invariant. If a caller feeds this payload through `__unserialize()`, the engine lands in the forbidden state, and everything downstream returns zero: `generate()`, `Randomizer::getInt()`, `Randomizer::getBytes()`, `Randomizer::nextFloat()`, and every other method that consumes the engine.
## Reproduction
~~~php
<?php
// Constructor guard works as intended:
try {
new Random\Engine\Xoshiro256StarStar(str_repeat("\0", 32));
} catch (\ValueError $e) {
echo $e->getMessage(), "\n";
// Random\Engine\Xoshiro256StarStar::__construct():
// Argument #1 ($seed) must not consist entirely of NUL bytes
}
// __unserialize() bypasses it:
$engine = new Random\Engine\Xoshiro256StarStar(42);
$engine->__unserialize([
[],
['0000000000000000', '0000000000000000', '0000000000000000', '0000000000000000'],
]);
$r = new Random\Randomizer($engine);
var_dump($r->getInt(1, 1_000_000)); // int(1)
var_dump(bin2hex($r->getBytes(16))); // "00000000000000000000000000000000"
var_dump($r->nextFloat()); // float(0)
~~~
## Expected
`__unserialize()` should reject the all-zero state the same way `__construct()` does. Returning `false` from the `unserialize` callback in `engine_xoshiro256starstar.c` is enough; the Mt19937-aliased `__unserialize()` wrapper turns that into the standard "Invalid serialization data for ... object" exception.
Without the guard, a bad serialized payload (hand-crafted, or round-tripped from an attacker-controlled source) produces a Randomizer that silently returns 0 from every method.
## PHP Version
master (PHP 8.6.0-dev). The `unserialize` callback has shipped without the zero-state check since Xoshiro256StarStar landed, so 8.3, 8.4, and 8.5 are affected too.