Skip to content

ext/random: Random\Engine\Xoshiro256StarStar::__unserialize() accepts all-zero state #21731

@iliaal

Description

@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:

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

// 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions