Skip to content

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

Closed

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.

Activity

  1. added 2 commits that reference this issue on Apr 12, 2026
    1a428e5
    73eace2
  2. added a commit that references this issue on Apr 12, 2026
    2181129
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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